Documentation
¶
Overview ¶
Package cli — `forge project annotations`: dump forge's authoritative entity-authoring annotation spec as structured data.
The command answers "what annotations does forge understand, and what do they do?" from forge's OWN definitions rather than from prose that drifts:
- markers the `// forge:*` comment markers the proto scanner reads (forge:entity, forge:soft-delete, forge:append-only, forge:read-only, forge:computed, forge:generated, forge:secret, …).
- field_types the proto→column mapping an entity birth applies, iterated from the real scaffold.ProtoSQLMappings (whose scalar half is projected from the renderer's scalarSQL).
- validate_rules the projected protovalidate subset, DISCOVERED by probing forge's own recognizer (codegen.ParseRawValidateOptions) with every rule protovalidate itself declares, and whose db/zod effects are COMPUTED by forge's own projection functions (codegen.FieldConstraints.SQLChecks / .ZodChain).
- service_options / method_options the (forge.v1.service) / (forge.v1.method) proto options, enumerated from the compiled forgepb descriptors.
Everything sourced from an iterable structure is derived, not transcribed, so the dump tracks the code. The genuinely descriptive fields (a marker's effect/example, an option's default) are thin literals whose source is cited at the definition site; a package test pins their names against the real recognizers/descriptors so a rename can never silently drift the dump.
Package cli — `forge api` command surface.
`forge api curl <service.method>` prints a copy-pasteable curl invocation for a Connect RPC endpoint. Connect handlers already speak plain HTTP/1.1 POST with application/json — no gRPC tooling needed — but the URL shape and Content-Type rules are undocumented in most projects. This command removes the discovery friction: look up the method's input message in forge_descriptor.json and emit a curl command with a request-body skeleton populated from zero values for each field, addressed to the conventional local listener (--port overrides).
Streaming RPCs are flagged but still printed — the body shape is the same; only the Content-Type changes to application/connect+json. We surface the difference rather than reject the command so users can hit streaming endpoints too (curl will get the first frame; this is enough to verify reachability and auth).
Package cli — `forge project audit` external-builds category (Phase 3 of the build_cmd / external-build escape-hatch refactor).
Surfaces every KCL service whose effective build is a ShellBuild. For each such service the category emits:
- the resolved build_cwd state (exists / missing on disk),
- the last build state from buildtarget.ReadState (image, tag, pushed_at) for every env that recorded one,
- any build_env keys that conflict with a built-in substitution token (IMAGE / TAG / SERVICE / PROJECT_DIR / REGISTRY / TARGETARCH / BUILD_CWD).
Sub-agents and CI dashboards can branch on `.external_builds.status == "warn"` (or "error") directly. The category honours the additive-extension contract from the audit-json skill — a new field on the per-service detail map is additive; consumers that don't read the new key are unaffected.
We render the dev env to enumerate services (matching auditIngress's approach) because that's the only env every project is guaranteed to have. The per-service ReadState loop walks every env declared under deploy/kcl/<env>/main.k so the snapshot reflects what each env last built, not just dev.
Status semantics:
- ok — no services declare a ShellBuild, OR every service has a present build_cwd, no env-key conflicts, and at least one env has recorded state (or no envs at all).
- warn — at least one service has a missing build_cwd on disk, OR at least one service declares a build_env key that collides with a built-in token. Neither blocks a build — skip-with-warn for cwd is the documented contract, and the built-in wins on conflict — but both are surface-up worthy.
- error — KCL render itself failed AND we know features.build is on (project intends to build something; we can't see whether external builds are configured). Degrades to warn when the failure is environmental (kcl not on PATH, no deploy/kcl/dev dir).
Package cli — `forge project audit` ingress category (KCL-entity-typed half).
auditIngress / ingressBackendNames / crossCheckIngress stay in package cli because they depend on the KCL render + entity structs (GatewayEntity / HTTPRouteEntity / GRPCRouteEntity) shared by ~12 cli files (build/deploy/dev/doctor). The audit command group (internal/cli/audit) reaches auditIngress through factory.AuditAPI.Ingress, so it never touches these entity types. They return the neutral audittype.Category the group consumes.
Package cli — `forge project audit` prerequisites category.
Surfaces the env's DECLARED external prerequisites (forge.ExternalSecret / forge.DNSRecord on Bundle.required_secrets / required_dns) — the out-of-band facts a deploy depends on but forge does NOT create. Today these prereqs live only in KCL docstrings, so `forge env deploy` renders green and THEN cert-manager's ACME challenge or the workspace-proxy DNS hangs silently because the prereq was never satisfied. Modeling them as first-class entities turns the docstring into:
- a render-time CHECKLIST (this audit category + the deploy banner),
- a deploy PREFLIGHT block on a declared-required-but-absent ExternalSecret (see cluster.Preflight / RequiredSecrets), and
- a cross-secret BYTE-MATCH consistency check: ExternalSecrets sharing a `value_group` carry the SAME logical value; this category reports the group membership (the KCL schema already blocks a group whose members declare different key sets, and the live byte-compare runs in the preflight).
Like auditIngress this renders the dev env (the only env every project is guaranteed to have) purely to ENUMERATE the declarations; the category is informational (status ok/warn), never an error — a declared prereq is a good thing, and whether it's SATISFIED is a live-cluster question the deploy preflight answers, not a static audit.
build_external.go is the SINGLE dispatcher for KCL Services whose effective build is a ShellBuild (`build = forge.ShellBuild { cmd, cwd, env }`) — the one shell escape hatch. Mirrors the deploytarget/External provider on the build side — same `sh -c` shape, same ${X} substitution, same fail-when-cwd-missing contract. See internal/buildtarget for the runner + spec types.
The dispatcher's responsibilities are deliberately narrow:
- Iterate KCL services whose effective build is a ShellBuild (EffectiveBuildCmd != "").
- Build a Spec from KCL fields + the build-loop's resolved tag / registry / target arch.
- Run Spec through buildtarget.Runner.Build.
- Persist per-service state when the build succeeded so a subsequent `forge env deploy <env>` can pin the same tag.
Build-side ownership boundary: the user's BuildCmd owns BOTH the build AND the push (the user composes `docker build … && docker push …` into one string). Forge does NOT run docker push afterwards. This matches the External (deploy) provider's "user owns the CLI" contract — one mental model across both escape hatches.
Package cli — `forge project capabilities`: everything forge can do for you, in one call, derived from forge itself.
WHY THIS EXISTS. A dogfood run measured "researching how forge wants things done" as the single largest time bucket on the way to a green gate. The surface is not hidden — every verb is under some `--help` — but reaching it costs one invocation per command group, so an agent that does not already suspect `forge scaffold rpc` exists writes the handler stub by hand. This command answers "what does forge already do?" in one call, which is the only shape that competes with guessing.
Everything here is ENUMERATED, never transcribed:
- commands the real cobra tree (cli.NewRootCmd), walked depth-first with each command's own Short. A verb that is added, renamed or removed changes this dump with no edit here.
- analyzers every flag on `forge lint`, INCLUDING the maintainer flags hidden from --help — a hidden analyzer is still a capability, and `--wire-coverage` / `--scaffolds` are documented nowhere else an agent will look.
- markers markerSpecs(), which a package test pins against the real recognizers in the proto scanner.
The deliberate omission is prose about WHEN to reach for each verb. That is what the skills are for; a transcribed opinion here would be the hand-maintained list this command exists to avoid.
Package cli — declarative cluster reconcile for `forge env up`.
This file generalizes the dev-only, single-cluster ensureDevCluster bootstrap into a declarative LIST: an env declares `Bundle.clusters = [forge.Cluster {...}, ...]` and forge ensures each exists at the head of `forge env up` (create-if-absent, no-op if present).
This file IS the k3d ClusterProvider's implementation (see cluster_provider.go for the registry that dispatches to it per-entity by Cluster.provider). Everything below — ensureDeclaredCluster, the create/start/heal seams, the node-level docker shell-outs — is k3d- specific; a "vcluster" or "gke" provider is a SEPARATE ClusterProvider implementation with its own file, not a branch added here.
Multi-cluster ownership is a REFERENCE. There is no "primary" cluster: a secondary cluster names its `owner` Cluster, and the KCL render layer DERIVES the joined docker network (Cluster.Network = `k3d-<owner.name>`) and the registry-inherit flag (Cluster.RegistryInherit = true) from that one edge. The owner cluster projects neither — k3d creates its own network/registry. There is no most-X heuristic. `owner` is meaningful for k3d only — see the Cluster schema doc (kcl/schema.k) for why a vcluster's host relationship is a distinct `host?` edge instead.
Package cli — the cluster-provisioning provider registry.
This mirrors internal/deploytarget's Provider/Registry shape (deploytarget.go), one level earlier in the pipeline: deploytarget dispatches a rendered SERVICE to the pipeline that ships it somewhere (k8s-cluster / external / compose / host-infra); this dispatches a declared CLUSTER to the pipeline that ensures it exists in the first place, before any service can be deployed into it.
Package cli — standalone k3d registry lifecycle for declared clusters.
A k3d "Simple" config can reference a registry in two ways:
- registries.create — k3d creates a registry container OWNED BY the cluster. `k3d cluster delete` destroys it. Every cold recreate then rebuilds + re-pushes every image (the fat ones especially).
- registries.use — k3d references an EXISTING registry container. A standalone registry (`k3d registry create`) is owned by NO cluster, so it survives `cluster delete` and its pushed images persist across cold recreates.
This file makes the persistent path zero-glue: when a declared cluster's k3d config references a registry via `registries.use`, forge ensures that standalone registry exists BEFORE `k3d cluster create` runs. Idempotent — an already-present registry is a no-op; an absent one is created with the declared host port so host pushes keep resolving to the same `localhost: <port>` ref.
Package cli — `forge project delete service` — the inverse of `forge scaffold service`.
Where `forge scaffold service` scaffolds a handlers/<svc>/ dir and (after the user lists its serviceRow line) serves it, `forge project delete service` walks that back:
- Removes the handler scaffold directory (handlers/<svc>/).
- Leaves a TYPES-ONLY tombstone comment in pkg/app/services.go in place of the serviceRow line. The comment is load-bearing: the registry treats any mention of the service name in that file as "deliberately not served here", so the proto types / Connect client / frontend hooks keep generating for callers while the handler scaffold, wiring, and auth registration are gated off. (See generate_serve.go's package doc for the registry semantics.)
This addresses the orphan-stub hazard (FORGE_SHAPE_REDESIGN §7f): a service added but never implemented previously had no inverse command, so it lingered as an Unimplemented CRUD stub with no consumers. Delete is the explicit retirement path.
Destructive (it removes a directory), so it confirms before acting unless --yes; --dry-run prints the plan and changes nothing.
Package cli — bridge from declared platform deps (forge.HelmChart) to the cluster apply pipeline's helm-as-a-RENDERER path.
The KCL layer projects each forge.HelmChart into the `output.helm_charts` contract (HelmChartEntity). This file turns those declarations into internal/cluster.HelmChartSpec values — fetching the forge-supplied CRD bundle each chart needs (the pinned standard Gateway API CRDs for Envoy Gateway; cert-manager's CRDs at the chart version) so cluster.Apply can apply CRDs-first/Established-gated before the chart's --skip-crds controllers.
The CRD-fetch REUSES the existing pinned-CRD machinery (dev_cluster_ingress.go: ingressPinnedVersions + the cached fetch) that the k3d `forge cluster up` path already owns — keeping internal/cluster free of the templates/cache dependency (cli imports cluster, not the reverse).
Package cli — `forge cluster` command tree.
The `cluster` subtree consolidates the universal local-cluster lifecycle mechanics every k8s-targeting forge project would otherwise hand-write in bash, plus the dev-state introspection commands:
- up/down/reset/reload (k3d cluster lifecycle)
- status (dynamic: cluster up/down, pods, ingress URLs)
- urls (ingress URL table for the env)
- logs / info / instances (per-namespace introspection)
kubectl port-forward isn't wrapped here — the Gateway API ingress path (forge cluster urls) is the canonical "reach a service from the host" entry point. Ad-hoc port-forwards for stateful workloads (database shells, debug metrics endpoints) are `kubectl port-forward` directly.
Project-specific orchestration (sibling-repo deploys, helm bootstraps, Stripe webhook listeners, project-specific seeds) is NOT owned by forge — projects keep those in scripts/ and Taskfile.yml, composed with the forge cluster primitives. See the `dev` skill for the boundary doc.
Package cli — `forge cluster` k3d lifecycle subcommands.
This file consolidates the k3d cluster lifecycle that every k8s-targeting forge project would otherwise hand-write in bash (~30-50 lines of idempotent create / delete / wait-for-rollout / context-pin logic).
k3d itself is the source of truth for cluster state — we shell out to `k3d cluster create/delete/list` rather than reinvent. The value forge adds is:
- read deploy/k3d.yaml as the canonical config (no hand-written --servers/--no-lb/--registry-create flags scattered across scripts)
- idempotent up/down semantics (`up` no-ops if the cluster exists, `down` no-ops if it doesn't)
- kubectl context pinning so `forge cluster reload` can't accidentally apply to staging or prod
- one-command reload that re-renders KCL + applies + waits for rollout (the inner loop during local dev)
Package cli — `forge cluster up` ingress install plumbing.
forge is OPINIONATED on ONE Gateway API controller everywhere: Envoy Gateway (controllerName gateway.envoyproxy.io/gatewayclass-controller, GatewayClass `eg`). The local k3d bring-up installs the SAME helm chart the cloud envs (staging/preprod/prod) run, so a Gateway that renders `gatewayClassName: eg` behaves identically on a laptop and in the cloud. Envoy Gateway serves HTTPRoute AND GRPCRoute natively, so one controller covers every forge route shape — no second controller for gRPC.
Three pieces, run in order after the k3d cluster is created and kubectl context pinned:
- Fetch + apply the upstream Gateway API standard-channel CRDs (version pinned via internal/templates/ingress/envoy/VERSION). Cached under ~/.cache/forge/ingress/ so subsequent cluster-up runs are offline-capable. The gateway-helm chart bundles the CRDs it needs, but we apply the pinned standard channel explicitly first (server-side) so the CRD surface matches the cloud install and so `kubectl wait` on Established gates the GatewayClass apply.
- `helm upgrade --install --skip-crds` the Envoy Gateway controller from the pinned gateway-helm chart version into envoy-gateway-system, the SAME release the cloud envs run. `--skip-crds` is required: the chart bundles an OLDER, experimental-channel copy of the Gateway API CRDs that the safe-upgrades ValidatingAdmissionPolicy (shipped by the standard CRDs applied in step 1) would DENY — see helmInstallEnvoyGateway. Envoy Gateway provisions a managed Envoy proxy per Gateway with listener sockets derived dynamically from Gateway.spec.listeners — no static per-listener entrypoint config to template (unlike the old Traefik install). The proxy's LoadBalancer Service ports follow the Gateway listeners; on k3d the bundled klipper servicelb binds those node ports and the k3d serverlb forwards the host ports mapped from deploy/k3d-ports.yaml.
- Apply the vendored `eg` GatewayClass (idempotent).
Idempotency comes from `helm upgrade --install` + `kubectl apply` semantics — re-running against a cluster that already has them noops. We block on CRD establishment between (1) and (3) so the GatewayClass apply doesn't race the CRD install.
Also: k3d config merging — `forge generate` writes `deploy/k3d-ports.yaml` derived from the dev env's KCL gateway listeners. At cluster-up time we read deploy/k3d.yaml + deploy/k3d-ports.yaml, merge their ports blocks in memory, and hand a temp file to `k3d cluster create`. Keeps deploy/k3d.yaml user-owned while keeping host ports in lockstep with the project's declared Gateway listeners.
Package cli — `forge cluster up` mkcert TLS provisioning.
Why this exists: in dev, cert-manager's only stand-alone option is a self-signed Issuer whose CA isn't in the host trust store, so every browser shows a warning and every curl needs `-k`. mkcert installs a CA into the OS trust store once (`mkcert -install`) and signs leaf certs the host already trusts. The cluster-side shape stays identical to prod (Gateway carries `tls:`, listener is HTTPS, Secret is referenced normally); only the cert origin differs.
This file walks the dev env's rendered KCL, finds Gateways whose tls.mode == "mkcert", and provisions a kubernetes.io/tls Secret per (hostname, secret_name) pair. Re-running just refreshes the Secret.
Package cli — `forge cluster info` command.
Diagnostic dump of the dev-loop config: which cluster, which context, which namespace, which service ports. Replaces the small bash recipe every project would otherwise hand-write to debug "why is my ingress URL hitting the wrong service?"
Package cli — `forge cluster instances` command.
Lists every forge-managed dev namespace on every reachable k3d cluster. Supports the multi-worktree workflow where each worktree runs in its own namespace and we need a host-wide view to find what's running.
Package cli — `forge cluster logs` command.
Streams kubectl logs for one or all services in the dev namespace. Replaces a small bash recipe (resolve namespace, look up the right label selector for a service deployment) that every project would otherwise hand-write.
Package cli — `forge cluster status` command.
Renders a human- or machine-readable snapshot of the dev cluster, running pods, and ingress URLs derived from rendered KCL. Replaces a 30-line bash recipe every k8s-targeting forge project would otherwise hand-write.
Package cli — `forge cluster urls` command.
Reads the rendered dev-env KCL and prints the ingress URL table — one row per HTTPRoute/GRPCRoute, grouped by gateway + listener. Lets users (and sub-agents) discover "what URL do I hit" without reading deploy/kcl/dev/ingress.k by hand.
Package cli — `forge env devstack` command tree.
The parallel-dev-stack primitives (ADR 0003) live in internal/devstack: the raw git facts pushed into KCL as options, and the memoized forge.allocate_port(base, key) block allocator. Those primitives are resolved INSIDE a KCL render (under the up/deploy activation path). But a host launcher — a Taskfile target, a bootstrap script — needs the SAME allocated host port BEFORE `forge env up` renders the KCL, so it can start the host `reliant` process LISTENING on exactly the port the in-cluster controller will dial.
`forge env devstack port <base>` is that single source of truth: it resolves the current worktree key (devstack.Worktree) and returns allocate_port(base, key) — base + block(key)*100 — through the SAME lock-guarded block registry (.forge/blocks.json) the KCL builtin uses, so the launcher and the render can never disagree on the port. On the PRIMARY checkout the key is "" ⇒ block 0 ⇒ the base is returned unchanged (no registry/lock touch), so the default dev loop is byte-identical to today.
`forge project disown <path>... --reason <text>` — one-way ownership transfer.
The file lifecycle has exactly two states and one door:
- forge-owned (Tier-1): regenerated on every `forge generate`; hand-edits are a hard drift error.
- user-owned ("yours"): never touched after emission.
- `forge project disown` is the one-way door from the first to the second.
There is deliberately NO fork limbo (forked-but-maybe-reconciled- later). A fleet-wide audit of every long-lived fork found zero that were justified — each was a bug, a missing API, a mis-tier, or staleness — while the limbo state produced the worst incidents (silently frozen wire_gen.go). Disowning is final by design; the documented re-adoption path is equally simple: delete the file and run `forge generate` — the emitter re-emits the pristine render and the entry returns to Tier-1.
--reason is REQUIRED. A disown means the generated code couldn't express what the user needed — that's design feedback, and the reason is the payload. It is recorded per path in .forge/disowned.json; `forge project audit --json` joins it back onto each disowned_files row.
`forge project disown <path>... --absent --reason <text>` — decline a scaffold-once file: forge never writes it in this project.
WHY. A scaffold-once file is yours from birth, and deleting it is the ordinary way to say "not this one": the birth ledger records the birth, so "recorded + absent" reads as deliberately absent and forge leaves it so. That only works for a birth that SURVIVES its run. Reliant moved its cobra tree to cmd/reliant/cmd/, forge scaffolded serve.go there, serve.go's presence switched internal/app composition on, composition failed on 104 missing providers, and the rollback removed serve.go together with its birth record — correctly, since a record for a file the rollback deleted would have consumed the path's only birth. Every later generate repeated the cycle, so there was never a record to delete the file against; the only way out was hand-writing JSON into .forge/scaffolded.json.
So declining is a first-class, declared act: a ledger entry carrying the reason (ScaffoldEntry.Declined), written by this command OUTSIDE generate. Every scaffold-once writer already asks the ledger before a birth (checksums.ScaffoldOnceDecision), so the decline is honored everywhere with no list for a writer to forget, and a failed generate — whose rollback restores the ledger to its pre-run bytes — keeps it.
Why the ledger and not a forge.yaml list: forge.yaml declares what the project IS, and scaffold ownership is already the ledger's subject — a second list in forge.yaml would be a second answer to "may forge write this path?", consulted by some writers and not others. The ledger is committed and human-readable, so the decline is still a reviewed line in a file under version control; this command is the safe way to write it.
Why `disown`: it is already the verb for "this path is mine, forge keep off". Without --absent it takes a forge-generated file as it stands; with it, it takes a scaffold's absence.
Package cli — Docker daemon reachability preflight for the cluster phase.
Every k3d operation IS a docker operation: k3d nodes are containers, the k3d registry is a container, the cluster network is a docker network. When the daemon is unreachable, each downstream shell-out fails on its own timeout with its own idiosyncratic wording — `k3d cluster list` reports "runtime failed to list nodes: ... 500 Internal Server Error", `docker build` reports something else again — and the user reads N cryptic cascading failures instead of one true cause.
Docker reachability is a genuine PRECONDITION of the cluster phase and it is cheap to test, so it is tested up front. This mirrors the argument the doctor tool checks make in their own package doc: surface the gap before the failure cascades.
The eager probe also SAVES wall-clock in the failure case. Docker Desktop's host-side apiproxy sits in front of a VM; when the VM is gone the proxy keeps dialing and only surfaces a 500 after its own ~20s deadline, once per call. One bounded probe replaces that repeated stall.
Package cli — `forge doctor` Docker daemon HTTP(S)-proxy check.
When the Docker daemon is configured with an HTTP/HTTPS proxy, ALL container egress — including image pulls and registry TLS handshakes — is routed through that proxy. A well-behaved forward proxy tunnels the registry's CONNECT request straight through and pulls work fine. But a TLS-intercepting proxy (Proxyman, Charles, mitmproxy, a corporate MITM box) terminates the CONNECT and re-originates TLS with its own cert. The container runtime, which has no trust path to that cert, sees the pull fail mid-stream — surfacing as ImagePullBackOff, "unexpected EOF", or "connection reset by peer" on the image layer. The proxy is invisible to the user (Docker Desktop silently inherits the macOS system proxy, commonly `http://http.docker.internal:3128`), so the failure is notoriously hard to trace back to its cause.
Doctor surfaces the proxy as a WARN, not a fail: a proxy can be perfectly legitimate (a transparent corporate egress proxy, a pull- through cache). We can't tell from `docker info` alone whether it intercepts TLS — only that one is present and that, IF it does, image pulls will break in a way that looks like everything except a proxy. The message names the symptom and the fix so a user staring at an ImagePullBackOff can connect the two.
The decision core (dockerProxyCheck) is pure — it takes the already- extracted proxy strings — so unit tests cover every branch without shelling out to docker.
Package cli — `forge doctor` external-builds checks (Phase 3 of the build_cmd refactor).
For every KCL service whose Service.BuildCmd is non-empty doctor emits three observations:
- warn when the resolved build_cwd (or projectDir when BuildCwd is unset) is missing on disk. Build-side semantics already skip-with-warn at run time; doctor surfaces the gap up-front so the user can fix it before invoking `forge build`.
- warn when the first token of BuildCmd (split on whitespace, take index 0) isn't on PATH. Heuristic — skipped when the command opens with `cd ` or contains a `KEY=value` env-var assignment, because those need a real parse pass that's out of scope for v1.
- info recording the substituted BuildCmd against a synthetic Spec, so the user can spot ${X} substitution errors before running build.
All three observations live under one doctor check per service (Name="external-build: <service>") so the report stays compact even for projects with many external-build services. The check status rolls up: any warn beats pass; the heuristic-skipped paths still emit the info line so the substituted command is always visible.
The decision core (buildExternalBuildDoctorChecks) is pure — it takes the resolved KCL service set + a PATH lookup function + a stat function. Production passes exec.LookPath / os.Stat; unit tests pass map-backed stubs. Mirrors doctor_tools.go's seam.
Package cli — `forge doctor` ingress checks (Phase 4 of the Gateway API ingress refactor).
Two new check categories layered on top of the standard doctor signal-based checks:
- GatewayClass present — for each unique gateway_class_name declared across every env's rendered KCL, verify a matching `GatewayClass` resource exists in the active cluster context.
- cert-manager ClusterIssuer present — for each Gateway with tls.cert_issuer set in any env's KCL, verify a `ClusterIssuer` resource with that name exists.
Both checks are best-effort observation: they degrade to "skipped" with a clear reason when kubectl isn't on PATH, the cluster isn't reachable, the CRD itself isn't installed, or KCL can't be evaluated. Doctor must never fail-loud on environmental gaps the user hasn't fixed yet.
The cross-check logic is factored into a pure helper (buildIngressDoctorChecks) that takes already-collected class / issuer names plus a kubectl-resource-checker interface, so unit tests can exercise every status outcome without a live cluster.
Package cli — `forge doctor parity <svc>` subcommand.
Detects when a service's effective env+config diverges between host-mode (`forge run <svc>`) and cluster-mode (`forge env deploy <env>`) projection — the "local wasn't representative of prod" class of bug that surfaces as a deploy failure on Friday afternoon.
The check is static: we compute what each mode WOULD see by composing the same inputs the real loaders compose (forge.yaml environments[<env>].config + KCL host env_vars on the host side; KCL cluster env_vars on the cluster side), then diff. No process is spawned, no manifest applied, no Secret read — just a structured report of where the two modes disagree.
Three categories of divergence:
- value_mismatch — same key set on both sides with different inline values. Always a bug (exit 1).
- missing_in_<side> — key set in one mode, absent in the other. Bug (exit 1) UNLESS the cluster side projects from a Secret (host's secrets_file is the documented counterpart but we don't load it — see secret_channel_divergence below).
- secret_channel_divergence — same key is sourced from secrets_file on host and `secretRef` on cluster (or one side references a ${SECRET_REF} placeholder). EXPECTED — the channels differ by design. Reported so the model can verify the key names line up, but does NOT fail the exit code.
The diff core (diffParity) is pure: it operates on two side-input structs the cobra path assembles from real loaders and tests assemble in-memory. Filesystem touches stay in the cobra path.
Package cli — `forge doctor` host-tool checks.
Forge shells out to a handful of third-party binaries (kcl, kubectl, docker, go, buf, k3d, git, npm, mkcert, ...). When one of those is missing the user sees a cryptic `exec: "kcl": not found` from wherever the binary was first invoked — usually deep inside a multi-phase pipeline that has already partially run. Doctor's job is to surface the gap up front with an OS-specific install hint so the user can fix the toolchain before the failure cascades.
The check decision core (runToolChecks) is pure: it takes function values for "is binary on PATH" and "run version command" so unit tests cover every status branch without exec-ing real processes.
The set of tools required for a given project depends on which `features.*` blocks are enabled (deploy needs kcl + kubectl; frontend needs npm; ingress with mkcert TLS needs mkcert host-side; etc.). Each toolCheck carries a Required(cfg, projectDir) predicate so the report skips tools the project doesn't actually use rather than emitting "missing optional tool" noise.
Package cli — `forge env`: the environment noun.
File: internal/cli/env_render.go
`forge env render <env>` — print the Kubernetes objects an environment renders, and the cluster each one lands on, without touching a cluster.
The question this answers is "what does this environment actually own", and until now forge had no way to ask it. `kcl run` on a real project fails, because a forge project's KCL calls kcl_plugin.forge.* and only forge registers that namespace. `forge doctor` renders every environment internally and then reports a COUNT ("5 env(s), 341 manifest(s)") — the objects it read never leave the check. `forge env deploy --dry-run` does print manifests, but it is the deploy command: it resolves a kubectl context, runs the declared-cluster guard, may stand up a k3d cluster and push images, and refuses outright when the recorded build is behind HEAD. None of that is available to someone who only wants to LOOK.
So the answer got reconstructed by hand, and the reconstruction is where the damage happens. An audit of which objects an environment owns read 1,886 lines of KCL and inferred each workload's target lexically — HOST from the presence of a `host = forge.HostOverrides` block, cluster from a per-service `k8s = K8sOverrides{cluster = ...}` override a thousand lines further down. One misread there deletes the wrong object out of the wrong cluster.
CLUSTER ATTRIBUTION is therefore not decoration, it is the point. An environment renders ONE manifest stream but may deploy it to SEVERAL clusters (control-plane's dev puts most workloads on k3d-control-plane and workspace-proxy on k3d-cp-daemon), and which document goes where is decided by internal/cluster.ScopeManifestsToGroup at apply time. This command calls THAT function, one document at a time, rather than modelling its rules again — a printer that disagrees with the router would describe a deploy that never happens, which is the failure mode it exists to prevent.
SIDE EFFECTS: forge cannot promise this is a pure read, and does not. KCL's `file.write` runs during evaluation, so a project whose deploy KCL generates a file (control-plane's dev main.k calls `nats.write_conf`, which materialises deploy/nats/nats.conf) writes it every time anything renders — `forge doctor` and this command included. forge has no hook to suppress a project's own writes: the write happens inside the embedded KCL runtime, which offers no read-only mode, and the alternatives (rendering from a copy of the tree, sandboxing the process) are neither portable nor cheap. What forge CAN do it does: it reverts its own render-state writes (the resolve_port store), and it watches the tree so the report is evidence rather than a promise. See renderWriteScan.
File: internal/cli/env_render_charts.go
Platform dependencies (forge.HelmChart) in `forge env render`.
A chart's objects are not in the env's KCL stream: KCL only DECLARES the chart, and `forge env deploy` expands it with `helm template` at apply time (internal/cluster.renderSelectedCharts). So a render that printed only the KCL stream showed 0 objects for flux, cert-manager and envoy-gateway while the deploy applied hundreds — the render was not a preview of the deploy, and no command could show a chart's manifests without contacting a cluster.
The fix renders charts through the deploy's OWN function (cluster.RenderChartStreams wraps renderSelectedCharts), with the deploy's own spec resolution (resolveDeployHelmSpecs: target selection, the declared- cluster refusal, the pinned CRD bundles) and the deploy's own choice of the cluster a chart without `cluster` lands on (helmPrimaryContext). Nothing here models what a chart renders; it only labels and attributes it.
File: internal/cli/env_shape.go
`forge env shape <env>` — the env's rendered DECLARATION, as a value.
This is the projection half of `forge env render`. Where render prints the manifests themselves — two megabytes of YAML for control-plane's prod — this prints what they ARE: the env's kind, what runs where, which secrets it needs, which domains it binds, and one hash per object. Small enough to store per environment, read per view, and diff as a set.
WHY IT IS A COMMAND AND NOT ONLY AN INTERNAL STEP. Three callers need the same answer, and all three must get the same bytes:
- `forge env build` and `forge env deploy` RECORD it on the control plane, which is what lets the Live view answer "what kind, which secrets, which provider" with no checkout and no daemon running;
- the reliant daemon exposes this command as `forge.env_shape`, which is what Preview's "Register" sends to EnsureEnvironment to bootstrap an env the control plane has never heard of;
- F7's `forge env diff` compares this projection against the recorded one.
They share internal/bundle.ProjectShape, so a shape a user read, a shape the control plane stored, and a shape a bundle sealed cannot disagree about the same render.
READ-ONLY, AND WATCHED RATHER THAN PROMISED. Like `forge env render`, this cannot guarantee purity — KCL evaluates `file.write` during evaluation and forge has no hook to suppress a project's own writes. So it does what render does and goes one step further: it scans the tree before and after, and a render that wrote anything is an ERROR naming the paths, not a warning. A caller registering an environment from this output is making a durable record, and a record derived from an impure render is a record nobody can reproduce.
Frontend source resolution — the bridge between a DECLARED cross-repo source and a directory a build can shell into.
A frontend declares its code one of two ways: a `path` (a directory in this repository) or a `source` (another repository, pinned to a ref). Every command that shells into a frontend — build, deploy, install, typecheck — needs a real directory, so this file resolves the second form to the first and leaves the first untouched.
The design constraint that shapes everything here: existing projects must be byte-identical. A project with no `source:` anywhere never constructs a resolver, never touches the cache, and never changes behavior — resolveFrontendSources returns immediately when nothing declares a source.
See internal/gitsource for the cache, the fetch, and the .forge/source-overrides.yaml local-override rules.
Frontend discovery for the CI workflow generator.
CI workflows need one fact about frontends: which directories in THIS repository hold a Node app, so the workflows can point setup-node at a package.json and run npm there.
That fact used to be read from forge.yaml's `frontends:` key. The key is retired — frontend topology moved to deploy/kcl/<env>/main.k, and `forge generate` strips a leftover block rather than honouring it — so the read silently returned an empty list on every real project. Empty meant HasFrontends=false and FrontendPath="", which the templates render as "no frontend jobs at all". Because the CI workflows are write-once scaffolds, nothing overwrote the stale blocks a previous forge had emitted while the key still existed, and the workflows froze pointing at whatever the frontend was called then. A rename (control-plane's admin-web → internal-console) left `frontends/admin-web` in e2e.yml, e2e CI broke, and no amount of regenerating could fix it: forge was reading a key that is always absent. The hand-edit that unbroke CI then failed the Tier-1 self-certification guard forever.
So discovery reads the KCL, the same source `forge build` and `forge env deploy` resolve a --target against.
Marker-driven stale-artifact cleanup.
Forge's pre-2026-06-05 cleanup walked generated-artifact directories and removed anything whose name didn't match a re-derived "expected" set computed from forge.yaml — and deleted user code when the re-derivation disagreed with on-disk snake_case proto layouts. The manifest era fixed that by deleting only manifest-recorded paths; the self-certifying era keeps the same safety property without the manifest: only files that carry a forge:hash certification marker are candidates. The marker IS forge's authorship record, embedded in the file itself — paths without one are user content and forge doesn't get to delete them.
A candidate is a marker-bearing file the current run did NOT re-emit (per the WrittenThisRun set). Guardrails before deletion:
- Owner-step gate. The tier1OwnerRegistry in generate_tier1_scope.go maps generated paths back to the step that emits them. A path whose owning step is gated off this run is left in place; that step wasn't going to write it regardless, so missing-from- WrittenThisRun is uninformative.
- Upgrade-managed paths. `forge project upgrade` is their emitter; `forge generate` deliberately leaves them alone.
- Scaffold-once paths. A path in the scaffold ledger is the user's, marker or not.
- Verification. A PRISTINE marker (certified machine output) is deleted: those bytes are exactly what forge rendered, so nothing of the user's goes with them. A file whose marker fails verification was hand-edited — the bytes are the user's now, so it is reported and kept unless --force-cleanup asks for it too.
- Disowned paths are never candidates.
Deletion is the default because a report nobody acts on leaves a stale file in the build: a factories test for an entity forge no longer generates keeps compiling against helpers that are gone, and the only way out used to be a flag --help did not show. Every delete is journaled, so a run that fails later rewinds it; and the sweep runs only in a full pipeline (no --steps preset or --templates-only run includes it) after every emitter, so "not written this run" means no emitter writes the path any more. The caller (stepCleanupStale) skips the sweep when a step failed as a warning: that step's outputs are unwritten for a reason other than staleness.
Scoped-fallback entries (.forge/hashes.json, comment-incapable formats) get the same treatment keyed off the recorded hash.
Config vs. filesystem cross-check — the "loud-by-default" guard.
Pre-2026-06-07 the generate pipeline silently skipped declared entities that had no on-disk backing, and silently ignored on-disk entities that weren't declared in forge.yaml. The asymmetry was the #1 source of "I added a service but generate did nothing" friction:
- forge.yaml declares services[].name=foo but proto/services/foo/ doesn't exist → generate runs, emits nothing for foo, exits 0.
- proto/services/foo/ exists on disk but forge.yaml lacks an entry → generate sees the proto but skips bootstrap wiring for it.
This file walks forge.yaml's declarations and the on-disk trees in parallel and collects every mismatch into a single batched report. stepLoadConfig calls validateConfigVsFilesystem after a successful LoadProject so the user sees the asymmetry the moment they run generate, not three steps deeper at a confusing "missing import" error.
Only what forge.yaml still DECLARES can be cross-checked here. Services and internal packages are discovered from their real sources, so there is no second list for them to disagree with — the declaration is the code.
Opt-out: --skip-config-check. We expose an opt-out (not opt-in) so the default path is loud — new adopters get the check unconditionally, and scripted callers in pathological setups (e.g. mid-migration where a proto dir exists transiently with no forge.yaml entry yet) can pass the flag to bypass without changing the steady-state default.
Reporting for contract_test.go scaffolds forge declined to write.
contract_test.go.tmpl emits exactly one call — `_, err := pkg.New(pkg.Deps{})` — which compiles only for the canonical single-`Service`, two-result-`New` package. Three package shapes are therefore declined rather than scaffolded, because writing the file anyway hands back a package that does not build.
Each decline used to print its own line, unconditionally. In control-plane that was ~25 lines on every run, describing 25 packages whose shape is a deliberate choice and is not going to change. They split into two kinds, and the two deserve opposite treatment:
A multi-interface package, or one whose interface is not named Service, is simply a package forge does not scaffold tests for. There is no advice in it — "write tests manually" is what the author is already doing. ROUTINE: behind -v.
A single-result `New` is different: `func New(Deps) (Service, error)` IS the shape forge's own conventions ask for, and polishing to it turns the auto-scaffold back on. That is real advice, so it survives at default verbosity — but as ONE line with a count, not one line per package. The per-package list is what -v is for.
The distinction that keeps this honest: a line stays at default verbosity when acting on it CHANGES something. "This package has two interfaces" does not; "these four packages would get scaffolded tests if you polished their constructor" does.
Disowned-sibling dangling-reference detection.
FRICTION 2026-06-04 (cp-forge layer-6 workers lane, fork era): the user had frozen `pkg/app/bootstrap.go` and `pkg/app/wire_gen.go` out of regeneration. After adding a worker, `forge generate` happily re-emitted `pkg/app/app_gen.go` — and the freshly regenerated `app_gen.go` declared `Workers *Workers`. But the `Workers` TYPE was defined inside the frozen `bootstrap.go`, still at its pre-workers content. `go build` then failed with `pkg/app/app_gen.go:42:13: undefined: Workers` — a silent build break far from its cause.
The same hazard exists for DISOWNED files (the frozen-file mechanics are identical: forge never re-emits them, even with `--force`), so the check survives the fork removal with disowned entries as its subject. Resolution: after codegen finishes, scan the regenerated forge-owned Tier-1 files inside the same package as any disowned sibling. For each referenced package-local type name that is NOT defined in either
- the file doing the reference, or
- any forge-owned sibling in the same package, or
- any disowned sibling in the same package,
surface a loud, actionable error listing the type, the call site, the disowned sibling that probably ought to have defined it, and the concrete escape hatches (hand-add the missing declaration to the disowned file — it's yours — or re-adopt it by deleting it and re-running `forge generate`).
The scan is restricted to the same Go package as the disowned file because that's where Go's package-local resolution applies — a dangling unqualified type name `Workers` in `app_gen.go` can only be resolved by a sibling .go file with the same `package app` clause. Cross-package references (e.g. `foo.Workers`) are qualified and would be flagged by `go build` with a different error class; they're out of scope here.
Per-file extension-point hints for Tier-1 drift.
When the stomp guard catches a hand-edited Tier-1 file, the worst thing the error can do is lead with `forge project disown`: agents take the path of least resistance, disown the file, and permanently lose regeneration (the failure chain this whole subsystem exists to prevent). The right answer is almost always "your customization has a designated user-owned home" — these hints name that home, per file shape, so the error message teaches the extension point first and the disown one-way door last.
── Why the hint must be TOTAL ────────────────────────────────────────
An audit of every disown in the fleet found ZERO that were deliberate, permanent ownership choices. Every one was a forge gap wearing an ownership hat: a missing extension point (the whole Connect interceptor / CORS / extra-route edge has no user-owned surface at all), a migration shield, or a gap that has since been closed and left the disown behind as dead weight.
The mechanism that produced that outcome is this function returning "". A blank hint reads as "forge has no opinion, do what you like", and the only remedy left on screen is the one-way door. So the hint is TOTAL: every Tier-1 path resolves to exactly one of two answers.
SEAM EXISTS → name the user-owned file and the hook. Editing the
generated file is simply the wrong move.
NO SEAM → say so, in those words, and name the seam that is
missing. A file with no extension point is a FORGE
DEFECT, not a project problem; the user's next action
is to report the named gap, not to quietly fork.
The NO-SEAM branch is the feature that replaces `forge project disown`. Disown's real payload was always the `--reason` string — the design feedback saying "forge could not express this". Naming the gap at the moment of friction captures that payload without also suppressing regeneration forever.
Fresh-render rescue for the Tier-1 stomp guard.
The guard reports a Tier-1 file as hand-edited when its bytes disagree with forge's record of them: the embedded forge:hash marker, or for a comment-incapable output (JSON) its .forge/hashes.json entry. That record can be stale while the bytes are exactly forge's own render:
- `forge generate` killed during "Validating generated code" had already rewritten three observability dashboards, but saves hashes.json only at the end of a run. The next generate refused all three as hand-edited Tier-1 files.
- A merge that takes one side's generated file and the other side's ledger, or merges a stamped file's body under one side's marker.
Forge must never let its own outputs and its record of them disagree in a way that blocks the next run. Writing the ledger earlier only narrows the window, and does nothing for a merge. Comparing against the truth does both: the guard quarantines each in-scope drifted path (its writes go to a side render, the on-disk file is untouched), every emitter runs, and stepAdjudicateTier1Drift — after the emitters, BEFORE validation, so a run that will fail does not pay for a build first — re-records every file whose bytes are this run's render (checksums.RescueFreshRender). Only what is left is a hand-edit, reported exactly as the guard used to, and the run rolls back.
The cost is rendering, which is cheap beside validation: a run with a genuine hand-edit now renders before it reports, where it used to stop at the guard.
Package cli — `forge generate --explain` provenance log.
Explain mode runs the normal generate pipeline and then prints a per-file traceability log: for each forge-tracked output, which source files / proto descriptors / contract.go inputs drove its generation, plus a "reason" field saying whether the file was rewritten or skipped because the input was unchanged.
Implementation choice: rather than instrument every individual generator (mock_gen, middleware_gen, crud_gen, …) with a callback, we read the post-generate state from forge_descriptor.json, the project filesystem, and the embedded forge:hash markers. The derived provenance is "approximate but useful" — exactly what the LLM caller wants. We can tighten it later by threading a callback through the codegen package, but we don't need to pay that cost up front to ship the most-useful 80%.
`forge generate --explain-drift` — show what regeneration would change before the user picks an escape hatch.
The Tier-1 stomp guard knows a file drifted (hash mismatch) but only has hashes for prior renders, not content — so it can't show the user WHAT changed. The actionable comparison is on-disk content vs a fresh render of the *current* templates: that diff is simultaneously "what did I hand-edit" and "what would --force destroy".
Mechanism: instead of aborting at the guard, --explain-drift marks every drifted path side-render-only (checksums.AddSideRenderOnly) and lets the pipeline proceed. The emitters render normally, but writes for drifted paths land at .forge/render/<path>; the user's on-disk content and the checksum entries stay untouched (the entries are snapshot-restored at the end because stepRehashTracked would otherwise re-stamp the drifted on-disk hash and silently bless the drift). After the step loop, each drifted file is diffed against its parked render via `git diff --no-index`, bounded, and the run still FAILS with the standard drift report — the flag explains, it does not approve.
Kept out of generate_pipeline.go (a parallel-edit hotspot): the pipeline file gains only the ctx fields and a three-line branch in stepCheckTier1Drift.
One-time pipeline migration off the legacy .forge/checksums.json global manifest onto self-certifying files (forge:hash markers).
The conversion rules live in internal/checksums/migrate.go. This file owns the pipeline integration:
- stepMigrateLegacyManifest runs between state load and the Tier-1 stomp guard. Pristine legacy entries get stamped; disowned ones convert to .forge/disowned.json; entries whose bytes match NOTHING the manifest recorded (kalshi fr-9a54388f0b: a manifest committed from a different work lane) are quarantined on ctx.LegacyUnverified with their writes side-render redirected.
- finishLegacyMigration runs after the step loop: each quarantined path whose fresh side render matches the on-disk bytes is proven pristine and stamped; everything else is stamped with the unverified-legacy sentinel and reported through the standard drift error (guard semantics: --force regenerates exactly the named files, `forge project disown` keeps them).
The legacy manifest is deleted by the migration itself — durable even when the run later aborts. The unverified sentinel keeps the guard honest across runs until the user resolves each file.
Missing scaffold-once artifact reporting.
FRICTION (measured dogfood run, 2026-07): an agent scaffolded a project, reviewed the born migrations, and correctly found three real schema defects in them (a nullable column that should be NOT NULL, an FK with no referential action, an index whose column order did not match the query). It fixed all three. That corrected schema invalidated the fixtures in the already-born internal/handlers/library/handlers_crud_test.go, whose literals were derived from the schema AS IT WAS AT BIRTH.
So it deleted the file, expecting `forge generate` to re-derive it against the schema as it now stood. Generate did not — correctly, because the birth ledger records that forge has already scaffolded that path and deleting a scaffold is an act of ownership (see internal/checksums/scaffoldledger.go). But generate also said NOTHING. It printed its success banner over a project that had silently lost a test file, and the author spent roughly fourteen further tool calls and an hour trying to reproduce the birth condition by hand — copying the project to a scratch directory, stripping the CRUD rpcs out of the proto, deleting db/migrations, deleting the handler directory — none of which could work, because the one thing that brought the file back was an edit to .forge/scaffolded.json that nothing in forge's output had ever named. That edit is now a command, `forge project rescaffold <path>` (rescaffold.go), and this notice names it.
The bug is not the suppression. The suppression is the feature, and this file does not touch it: forge still never resurrects a file the user removed. The bug is that the suppression was INVISIBLE, and an invisible correct decision is indistinguishable from a malfunction.
So generate now reports it, once per absent path, naming the FILE and the exact ACTION that restores it. Deliberately GENERAL over the ledger rather than special-cased to the CRUD lifecycle test: forge scaffolds many one-shot files (internal/app/auth.go, cmd/<bin>/main.go, the frontend pages, the per-RPC handler stubs), every one of them can be deleted for the same reason, and a check that knew only about handlers_crud_test.go would be overfitted to the single instance that happened to be measured.
It is a NOTICE, not a warning, and it does not route through warnOrFail: an intentionally deleted scaffold is a supported end state — the author who does not want forge's lifecycle test removes it and is done — so this must never be promotable into a --strict failure that punishes a project for a legitimate choice. What the author needs is the sentence they could not find, not a build that stops.
Package cli — typed step plan for `forge generate`.
Pre-2026-05-06, runGeneratePipeline was a 584-line procedural function holding 25 numbered ordered steps gated by 93 Features.*Enabled() checks. New steps were 30 lines of boilerplate appended to a numbered-comment sequence (Step 0a, 0b, 0b.1, ..., 8d-iii, 8f.1) — the numbering itself pleaded for a data structure (FORGE_REVIEW_CODEBASE.md Tier 1.1).
This file replaces that procedural blob with a typed []GenStep plan. Each step is a small named function operating on a shared pipelineContext. The pipeline becomes a loop over the slice that:
- calls step.Gate(ctx) — pure, side-effect-free predicate; false skips
- calls step.Run(ctx) — the action, returning an error to abort
The shape unblocks several downstream wins documented in the codebase review:
- --plan / --explain print the plan without executing it
- a watch loop re-runs only steps whose Tag matches changes
- per-step unit tests against a synthetic pipelineContext
- one-time parse of services/entities into ctx (avoids re-parsing ParseEntityProtos 3× — see Tier 2.5)
As of 2026-05-07 the entire pre-refactor pipeline is now flat: every numbered legacy step has a dedicated stepXxx entry below, and runMidPipelineLegacy is gone. The single remaining shared-state hop (parse services + module path once for steps 4-6) is its own GenStep.
(2026-05-06 polish-phase, completed 2026-05-07) — closes FORGE_REVIEW_CODEBASE.md Tier 1.1.
`forge generate --plan` and the loud-by-default helpers.
The plan mode complements --explain (provenance log after the run) and --check (drift detector that runs the pipeline) by inspecting the pipeline WITHOUT running anything: it prints the step list with a [RUN] / [SKIP] annotation per step, derived from each step's Gate(ctx) against the configured project state, then exits.
This is the diagnostic that answers "what would forge generate do here?" without the side-effects of actually doing it — useful when planning a big refactor, debugging "why didn't generate touch X?", and writing CI guards that need to know whether a pipeline change would alter the step set for a given project shape.
The warnOrFail helper lives here too because it shares the loud-by- default thesis: a step that routes its failure through it lets the run continue with the steps that do not need it, and the run still exits non-zero; --strict stops the run at that step instead.
Rename detection for Tier-1 Go emitters.
FRICTION 2026-06-02 (cp-forge dogfood pass): when `forge generate` renames a public symbol in a Tier-1 Go file (e.g. `db/embed.go` flipping `var Migrations` to `var MigrationsFS`), hand-written callers elsewhere in the project keep referencing the old name and the build breaks two runs later — long after the codegen step that did the rename has scrolled out of the user's terminal.
Resolution: a two-step pre/post pass around the codegen body of the pipeline. Pre-codegen we snapshot the public exports of every tracked Tier-1 Go file; post-codegen we re-extract exports from the freshly written files and diff. Names that disappear are candidates for stale references — we grep the project (skipping generated files and the renamed file itself) for `pkg.Name` and `Name` patterns and surface each call site as a warning.
The warnings are advisory by default. Future work: --strict-renames converts them to a hard pipeline error.
Auto-retire obsolete disowns.
`forge project disown` is a last-resort one-way escape hatch: it tells forge "stop regenerating this Tier-1 file, the bytes are mine now". Needing it routinely means a missing extension point. A disown that has become OBSOLETE — forge no longer Tier-1-owns that path — is dead weight: it protects against an overwrite that can no longer happen, and it misleads users into thinking disown is normal.
The canonical case: a project disowned a frontend page.tsx under an OLD forge that regenerated page.tsx as Tier-1. The current forge makes page.tsx Tier-2 (scaffold-once: written if absent, NEVER overwritten). The disown is now meaningless — forge would never overwrite a Tier-2 file anyway — yet it lingered across version migrations and had to be hand-dropped.
THE FIX: during `forge generate`, after every Tier-1 emitter has run, detect disowns whose path is NO LONGER a current Tier-1 emit target and auto-retire them (remove from .forge/disowned.json) with a loud per-path notice. A disown is:
- STILL VALID when its path IS a current Tier-1 emit target — forge WOULD regenerate it but for the disown. The target set (checksums.Tier1TargetSet) records every path a Tier-1 writer touched this run, BEFORE the disown-skip, so a disowned-but-live Tier-1 path is still in the set. Leave it alone.
- OBSOLETE when its path is NOT a current Tier-1 emit target — it became a Tier-2 scaffold-once file, or forge stopped emitting it. Retire it.
Conservatism: a disowned path can be absent from the target set for a reason unrelated to tiering — the emitter that WOULD own it was gated OFF this run (e.g. a frontend file under features.frontend=false). In that case absence is uninformative and we must NOT retire. The `targetable` predicate below gates retirement on "an emitter that could own this path as Tier-1 actually ran this run", so a gated-off subsystem never sheds its legitimate disowns.
What a binary serves is CODE, not config.
The serving decision lives in the user-owned pkg/app/services.go: RegisteredServices lists one generated serviceRow<X> call per service this binary serves. forge scaffolds that file once (listing every current service — zero semantic change for existing projects) and never rewrites it; from then on the row list is the single source of truth, and this file derives the served-set from it by AST-parsing the registration file (the same user-file-parsing posture as codegen.ParseServiceDeps and the setup.go scans).
Classification per service (see serviceRegistry.state):
REGISTERED — a serviceRow<X> identifier matching the service is referenced in services.go. Full treatment: handlers scaffold, CRUD/mocks, wire/diagnostics, auth-middleware skip-list, bootstrap row (via the user's list). UNLISTED — the service name appears NOWHERE in services.go. Treated as newly added (forge scaffold service / hand-edited forge.yaml): the handlers scaffold and row constructor still generate so the user can implement-then-register, but the service is NOT served — no auth skip-list entries, `forge env up` skips it, and `forge project audit` warns that the row constructor is unreferenced. The registration line is written by the USER (or their agent) — forge prints it but never edits the file. That's the design: the LLM writes the one decision line; forge generates the guardrails. TOMBSTONED — the service name appears in services.go only in a comment (the scaffold instructs: delete the row, leave a comment naming the binary that serves it). Types-only: proto types, Connect client, frontend hooks, and descriptor entries still generate (callers need them); the handlers/<svc>/ scaffold, row constructor, wire/diagnostics, and auth registration are gated off. The comment is load-bearing — without any mention the service reverts to UNLISTED and its scaffold regenerates.
Fallbacks: a missing services.go means "everything registered" (pre-migration projects keep today's behavior; the generate pipeline then scaffolds the file with exactly that meaning). A services.go that does not PARSE is a hard error in the generate pipeline (the build would fail anyway; better to name the file) and fail-open (everything registered) for read-only commands like audit/graph, which must not die on a broken tree.
Retirement of a pre-existing handlers/<svc>/ scaffold reuses the manifest-driven stale sweep (generate_cleanup.go): the gated emitters stop writing the tracked Tier-1 files, so they fall out of WrittenThisRun and are removed (the hand-edited ones only under --force-cleanup); Tier-2 user-owned files in the same dir are never candidates. `forge project audit` surfaces both halves via the codegen category's unregistered_services finding.
`forge generate` step: retire previously delivered agent skills.
Forge never writes skills into a project. Reliant reads forge's skills straight from the binary (`forge skill list|search|load`), and an on-disk render is a second, staler copy that can shadow the live catalog. Older forge versions did render `.claude/skills/<flat>/SKILL.md`; this step removes those forge-owned copies on the next generate. It never deletes a user-authored or user-modified file (see gcRetiredAgentSkills).
Harnesses without reliant's binary-backed catalog get a thin pointer skill (.claude/skills/forge/SKILL.md) written once by `forge project new`. It carries no banner or hash marker, which is what keeps this sweep from treating it as a retired render.
This sweep can be deleted once no project is likely to still carry delivered copies from a pre-retirement forge.
Stale scaffold-test detection.
FRICTION (front-door P0, 2026-06): the scaffold proto explicitly instructs renaming Item to the real entity. Following that instruction leaves handlers/<svc>/handlers_scaffold_<rpc>_test.go — ONE-SHOT, user-owned file rendered from internal/templates/service/unit_test.go.tmpl, never regenerated, no checksum entry — referencing deleted pb types (pb.CreateItemRequest, …). `forge generate` succeeds because the final validate step is `go build`, which does not compile _test.go files; the user then hits an immediate `go test` / `go vet` failure pointing at a file forge wrote, with no hint that the file is theirs to fix.
Resolution: NO auto-regen of user-owned files. After codegen has refreshed gen/, scan each handlers/<svc>/handlers_scaffold_*_test.go (and only those — keep scope tight), resolve its `pb "<module>/gen/…"` import to the on-disk generated package, and cross-check every `pb.<Ident>` reference against the names that package actually declares. Any referenced-but-undeclared ident → one one-line warning naming the file and telling the user the file is theirs: delete it or update the rows. Default is a warning (generate still succeeds); --strict promotes it via the standard ctx.warnOrFail helper.
Tier-1 stomp-guard scoping.
The pre-pipeline stomp guard (stepCheckTier1Drift) used to fail on ANY drifted Tier-1 file the scan found, even when the current `forge generate` invocation would never re-emit that file. In multi-lane migrations that hard-failed the guard for sibling work (e.g. agent A is porting internal/proxy/ and the guard rejected because agent B left pkg/app/migrate.go drifted in a separate changeset). FRICTION 2026-06-02: cp-forge dogfood pass.
Resolution: the guard now filters drift to the set of paths whose owning emitter step would actually run for this pipelineContext. A drifted file emitted by a step whose Gate returns false this run is silently ignored — that step wouldn't touch the file, so its drift cannot manifest as a stomp.
The path → owning-gate registry is intentionally explicit (small map at the bottom of this file). Adding a new Tier-1 emitter SHOULD add an entry here so the scoping logic stays accurate; the registry is fail-open (an unmapped path falls through to "in scope" so missing entries err on the safe side of preserving the loud-fail behavior).
The registry is keyed by exact path, trailing-slash prefix, or suffix-glob (path/segment pattern with a leading '*' wildcard). The matcher walks the entries in declaration order and returns the first match, so order from most-specific to least-specific.
Tier-2 (user-owned-after-scaffold) exemption for the drift scan.
Some forge-certified files are deliberately NOT stomp-guarded even though they carry a forge:hash marker: the upgrade-managed "checksum-protected" starters (Dockerfile, Taskfile.yml, .golangci.yml, …) and the one-shot .github scaffolds. The marker's job there is to let `forge project upgrade` distinguish "still the pristine scaffold" (auto-update on version bumps) from "user customized it" (skip) — editing them is SANCTIONED, so a failed verification must not abort `forge generate`.
The probe itself lives in internal/generator (drift.go), next to Tier2ManagedPaths — the registry that defines the exemption — so that every gate that reports hand-edited generated files (this stomp guard, `forge lint`'s generated-file drift gate, `forge project audit`, `forge ci verify-generated`) reads one set and tells one story. This file is the pipeline-side handle on it.
(Historical note: this file used to flip stale tier=1 manifest entries to tier=2. The manifest is gone — the reclassification story is now: the legacy-manifest migration never stamps Tier-2-managed paths, the Tier-2 writer un-stamps reclassified pipeline outputs on their next scaffold pass, and this exemption covers the rest.)
Run-scoped verbosity for `forge generate`.
The default output of `forge generate` is PROGRESS plus things that need ACTION. Everything else is available on demand behind `-v/--verbose`, which the command already has.
The measurement that motivated this: control-plane on a clean tree, exit 0, zero files changed, printed 269 lines of which 58 were notices — 22 "Skipped internal/handlers/X/ (all handlers up to date)" lines, ~25 "Skipped contract_test.go scaffold for internal/X/" lines, and a "Preserved 15 existing CRUD page(s)" line. Every one of them reports that forge correctly did nothing, and every one of them is identical on the next run. Output that is always present carries no information: the reader learns to skim the whole block, including the two lines in it that did need an answer.
Why a package-level variable rather than threading a flag through: these lines are printed by helpers several layers below the pipeline context (generateServiceStubs, birthContractTest, writeCIScaffold), and they print to stdout via fmt.Printf rather than to a writer anyone owns. Threading a bool through every signature to reach a global stdout is ceremony around a value that is already global. The pipeline sets this once per run, under the same lock that serializes runs (generateMu plus the cross-process file lock in runGeneratePipelineFlags), so concurrent mutation is not reachable.
Package cli — `forge project introspect` command group.
Introspect surfaces "what the binary will actually register" without requiring the binary to be running. The current leaf is `handlers`, which walks the project's proto services and prints every RPC path in the canonical Connect form `/<package>.<service>/<method>`.
Use case: you wired a new service and want to confirm at a glance that every RPC will be reachable at the expected URL — catches "this service isn't wired" issues in seconds rather than via curl-debugging a running server.
File: internal/cli/kcl_eval.go
`forge kcl eval <file> [-S <path>]…` — evaluate ONE KCL file of this project and print a field out of it.
Why this command exists ¶
forge #277 made the `forge` KCL module come from the forge binary, so that the binary rendering a project IS the module version it renders against. A consequence, and an intended one, is that `kcl run` on a project file that says `import forge` cannot work: there is no dependency to fetch and no copy on disk. Nothing but forge evaluates a forge project's KCL.
That left a real gap. A project keeps plain declarative constants in library files — control-plane's deploy/kcl/lib/kata_pool.k declares the GKE node-pool inputs its build scripts need, lib/daemon_placement.k declares which cluster each env's daemon pods land in — and those files are read by scripts and tests, never deployed. `forge env render` evaluates an ENVIRONMENT into Kubernetes objects, which is not that. So there was no forge command for "evaluate this file, give me this field".
What filled the gap went around forge, which is the thing that must not happen. control-plane grew scripts/lib/kcl-forge-module.sh: make forge materialize its module into FORGE_KCL_MODULE_CACHE, dig the directory out of the cache, then run `kcl run <file> -S <field> -E forge=<dir>` — forge's own render setup, reconstructed from outside, pinned to the cache layout and the module name, and needing `kcl` on PATH that forge itself has not needed since it embedded the runtime. The same reconstruction sat in a Go test (internal/operators/shared/daemon_placement_test.go). Every one of those is a forge defect that got worked around instead of reported, and the fix is a forge command, not a better script.
Why `forge kcl eval` and not `forge env render --file` ¶
Because it is not a render of an environment, and every flag `env render` has says so. That command resolves an image tag from build state, applies digests, templates declared helm charts, and attributes each object to the cluster `forge env deploy` would apply it to — `--tag`, `--cluster`, `--kind`, `--target`, `--no-digest`, `--no-charts` are all meaningless for a library file that declares seven strings. Hanging `--file` off it would put the whole env surface in front of callers who want one constant, and would have to answer what `--cluster` means for a file with no workloads.
A `kcl` group also says the true thing about scope: the unit is a KCL file, and the group is where a future `forge kcl fmt` or `forge kcl options` belongs. `env render` remains the way to evaluate an environment.
Read-only ¶
The evaluation claims nothing. The read-only halves of the render context are armed (see armReadOnlyKCLContext): fp.allocate_port resolves keys to blocks they already hold, fp.resolve_port reads its store without writing it, and fp.write_file is disarmed. A read-only command never claims a port block (#272). KCL's own `file.write` is not forge's to suppress — see `env render`'s note on the same subject — so a file that generates a file on evaluation still does, and that is reported on stderr.
File: internal/cli/kcl_imports.go
`forge kcl imports` — print where each forge project this project's KCL imports ([forge_projects] in deploy/kcl/kcl.mod) resolves to.
A render reads an imported project's KCL from wherever go.mod resolves its module: a replace directory, a go.work use, or the module cache at the required version (internal/kclprojects). That location is a fact a person debugging a render needs ("which reliant did this import?"), and one a Go test that renders through a forge child process needs too: the child's reads are invisible to Go's test cache, so a cache-sound render test stats the imported files itself, and this is how it learns where they are.
MAX-PUBLIC-STRUCTS IS SUPPRESSED HERE, AND IT IS RECORDED DEBT, NOT A NIT.
revive counts exported structs per PACKAGE and reports them on whichever file it reaches first, which is this one. So the finding is not about the types below in particular: package cli declared 68 exported structs against a ceiling of 40 before this branch existed, and it has been over the line for a long time. Nothing added here could bring it under 40 — the 13 types this branch adds would have to become -28 — so a suppression is the only thing that makes the gate report the truth rather than attribute 28 pre-existing declarations to the commit that happened to touch the file.
The types are exported for ONE reason: they are encoding/json unmarshal targets for the JSON the sibling KCL deploy module emits, and json.Unmarshal can only populate exported FIELDS. Go requires no exported type for that, but the fields must be exported, and the house convention here keeps the type and its fields at the same visibility so a reader is not left wondering why a lowercase type has uppercase members. Verified before suppressing: none of the types this branch adds (RemoteBuild, RemoteBuildSource, ControlPlaneEntity, FrontendRuntime, BucketRuntime, FirebaseRuntime, StaticSiteCDN, CacheRule, BundleDir) is referenced outside package cli, so every one of them is package-private in practice.
THE REAL FIX IS A PACKAGE SPLIT, and this comment is the note that says so. The KCL entity schema — every *Entity, every deploy/build variant, roughly 50 of the 68 — is a self-contained JSON contract with no dependency on cobra or on any command, and it belongs in its own package (internal/kclentity or similar) that owns it. That lands package cli under the ceiling honestly and lets this directive be deleted. It is a mechanical but wide move that would bury this branch's diff, so it is deliberately not done here.
Package cli — forge KCL module migration.
A project's env main.k files `import forge`. forge supplies that module from the binary that is rendering (internal/kclvendor → internal/kclrender), so the project's deploy/kcl/kcl.mod declares NO forge dependency and the project holds no copy of the module.
Older forge versions declared it — first as an unpublished `kcl-vX.Y.Z` git tag, then as `forge = { path = "../../.forge-kcl" }` over a project-local copy — and this step migrates those projects forward: it removes the declaration (and the marker block forge maintained around it), strips the forge entry from kcl.mod.lock, and deletes `.forge-kcl/`. See docs/adr/0003-kcl-module-from-the-binary.md.
Package cli — cross-cluster kubeconfig minting (forge.KubeconfigSecret).
In a multi-k3d-cluster dev/e2e env, a workload in cluster A may need to talk to cluster B's API server. A kubeconfig with B's serverlb IP baked in goes stale every time B is recreated (k3d hands the container a fresh docker-network IP). This file mints that kubeconfig FRESH each `forge up` — resolving the endpoint at mint time, never persisting the IP — and stores it as a k8s Secret the workload mounts.
The "in-network" reachability seam is the dev/e2e path: the target's API server is only reachable by its serverlb container IP on the shared docker network. That IP isn't in the serving cert's SANs, but the container's NAME is, so the minted kubeconfig dials https://<ip>:6443 and verifies the certificate against the cluster CA as tls-server-name k3d-<name>-serverlb (see rewriteInNetworkKubeconfig — verification is never skipped). "endpoint" (prod) uses the kubeconfig's own endpoint verbatim — a stable reachable address with a valid cert.
Package cli — minting a POD-USABLE kubeconfig for any cluster forge can reach (forge defect F1b).
The sibling file kubeconfig_secret.go mints a kubeconfig by COPYING the operator's own credential for the target cluster. That works for k3d, whose kubeconfig holds a client certificate usable from anywhere, and it does not work at all for a managed cluster. A GKE kubeconfig authenticates through an `exec` plugin:
users:
- name: gke_p_us-central1_prod
user:
exec: {command: gke-gcloud-auth-plugin, ...}
which is an instruction to run a binary on the operator's machine using the operator's application-default credentials. Copied into a Secret and mounted into a pod it is inert — no plugin binary, no credentials, no authentication. That is why a hub kubeconfig had to be hand-made out-of-band, and it is what this file removes.
The fix is to stop copying and start MINTING. forge converges a ServiceAccount, the declared permissions, and a long-lived token Secret ON THE TARGET CLUSTER, then assembles a kubeconfig whose only credential is that token, inline. Nothing in the result refers to the operator: it is usable by any pod holding the Secret, which is the entire requirement.
Package cli — `forge project libraries`: where forge/pkg actually is on THIS machine, and what each subpackage is for.
WHY THIS EXISTS. A dogfood run caught three independent agents running a full-disk `find / -path "*forge/pkg/svcerr*"` in the same session, hunting for library source. Every one of them already knew the package name; what they could not answer was "where is it, and what else is in there". The existing answer — a `forge-libraries` skill — was loaded by nobody, and its hand-written table had drifted anyway: it listed a `pkg/dialects` that does not exist and omitted nine packages that do. A stale index nobody opens is not a routing mechanism.
Everything here is ENUMERATED, never transcribed:
- the DIRECTORY comes from `go list -m`, so it is the go toolchain's own answer for the pkg version THIS project resolves — a go.work replace in a dev tree, the module cache in a release build, without this code knowing which.
- the PACKAGE SET is the subdirectories of that directory.
- each PURPOSE is the package's own doc comment, read from that source.
So the output cannot drift from the packages, and it describes the version the project actually compiles against rather than whatever the forge binary was built beside.
File: internal/cli/libraries_signatures.go
The deep half of `forge project libraries`: not just which forge/pkg packages exist, but what each one EXPORTS, with real signatures.
WHY THIS EXISTS. The one-line-per-package inventory answers "does svcerr exist" — a question nobody was asking. A measured ten-unit fan-out wave issued 36 `go doc` calls out of 53 total unit bash calls: `go doc .../svcerr` in four separate threads, `go doc .../tdd RPCCase` in three, `go doc .../orm Context` in three. Every one of those threads had already been handed the inventory. They went to the toolchain because a synopsis cannot answer "what are svcerr.WithReason's parameters" or "what are the fields of tdd.Case" — and because the inventory's closing paragraph told them to go run exactly that command.
The web-runtime half of this same verb has always listed symbols, and pasting THAT half into a unit brief demonstrably worked. This is the Go half catching up to a shape the command already proved.
Everything here is ENUMERATED, never transcribed, on the same rule the rest of this verb follows:
- the DIRECTORY is still `go list -m`'s answer (resolveForgePkgDir), so these are the signatures of the pkg version this project compiles against, not of whatever the forge binary was built beside.
- the DECLARATIONS are parsed out of that directory's own .go files and re-printed by go/printer, so a signature here IS the signature in the source — not a summary of one, and not a copy of one.
- the FILTER is Go's own export rule (ast.IsExported), so a package that gains an exported symbol gains a line here with no edit anywhere, and one that unexports a symbol stops advertising it.
There is deliberately no curated symbol list and no curated package list. A hand-maintained API digest is a lie with a timestamp: the hand-written `forge-libraries` skill this verb replaced listed a `pkg/dialects` that never existed while omitting nine packages that did.
What IS cut is prose. The parse mode below excludes comments outright, so no doc text can reach this output even by accident — the synopsis line the inventory already prints above each block is the package's description, and the signatures are its API. `go doc` on the five packages the wave actually hammered is ~17,000 characters, the large majority of it doc comments.
Package cli — `forge project map` command.
Map is a tree-shaped view of the project with ownership annotations on every leaf. The annotations come from cross-referencing:
- The embedded forge:hash certification marker (verifies = forge-space pristine; fails = forge-space drifted), plus the scoped .forge/hashes.json fallback for comment-incapable formats.
- File-name conventions (*_gen.go = forge-regenerated).
- Forge banner check (`Code generated by forge` first line).
- FORGE_SCAFFOLD marker presence (scaffolded but not finalized).
- Health flags: hand-edits to forge-space files (drift), proto/db entities not represented in migrations, etc.
Filtering and depth control mirror the spec: --depth N, --filter <subtree>, --json. The non-JSON output is plain-text indented tree (no box-drawing chars in the source — we render them at print time so the file stays grep-friendly).
Package cli — shared helpers for `forge project map` (and formerly `forge project audit`, which moved to the internal/cli/audit group). These walk the project tree to classify migrations and forge-generated files. They stay in package cli because map.go consumes them directly; the audit group keeps its own copy of isForgeGeneratedBanner (it's a five-line stdlib wrapper, duplicated per cmdutil's policy rather than shared through a leaf package).
Package cli — `forge project`: the project-structure noun.
Package cli — the shared reconcile spine under `forge env up` and `forge env deploy`.
Both commands bring an environment toward its declared end-state. WHICH entity kinds each acts on is structural, not a value threaded through the call:
forge env deploy — the cluster apply (in-cluster workloads, compose
deploy targets, the External dispatch) plus the
Firebase frontend publish.
forge env up — that same cluster apply, plus the concurrent infra
pre-warm, plus the host-process and frontend
dev-server phases. It always runs the whole loop.
`up` used to carry a `scope` value masking phases off per --cluster-only / --host-only. Those flags are gone: `--target <name>` scopes a run by naming the entities it acts on, which narrows the work inside each phase without forge deciding that a cluster-declared service is "really" a host process. Placement stays where it is declared — a service that should run on the host during dev says so with a `host` block in its KCL.
What remains genuinely variable is:
lifecycle — what a run does once everything is started:
* once — reconcile and RETURN. `forge env deploy` is
always `once`; so is the non-TTY `forge env up`
(start host/frontend processes, persist
their PIDs, print the summary, return —
stop later via `forge env down`).
* supervise — start the long-lived host/frontend
processes, HOLD on a signal channel, and
cascade-teardown on Ctrl-C. The interactive
`forge env up` lifecycle.
* auto — defer the once-vs-supervise choice to a
TTY check at runtime (resolveUpLifecycle).
`forge env up`'s default when neither --watch
nor --background is given.
In this vocabulary:
forge env deploy <env> = lifecycle=once, opts=<from flags>
forge env up <env> = lifecycle=auto, opts={skipFrontend: true}
The surgical knobs (tag / prune / dry-run / context override / targets / skip-frontend) live on deployOptions — the cluster reconcile's option surface, shared by both commands. `up`'s cluster step passes its deployOptions through the SAME named entry point deploy uses (reconcileCluster), so there is no blank-`deployOptions{}` literal standing in for "deploy with no options."
`forge project rescaffold <path>...` — have forge write a scaffold-once file again.
A scaffold-once ("yours") file is written exactly once and then belongs to the user. The birth ledger (.forge/scaffolded.json, committed) is what makes that true across deletion: an absent file with a birth record was deleted on purpose, and `forge generate` leaves it deleted in every clone. That is a feature — a repo that runs its own CI deletes forge's workflows and they stay gone — and it is not negotiable here.
What was false is the way back. Every scaffolded workflow said "`forge generate` re-creates it only if you delete it first", and generate did no such thing; it even printed "exists — leaving it untouched" about a file that was gone. The real reset was a hand edit of the ledger, and for the files only `forge project new` writes (the pre-commit config and workflow, the devcontainer, ...) there was no renderer left to run at all.
This command is the one supported reset. For each ABSENT path it drops the birth record and writes the file the way forge would scaffold it today, then records the birth again — so the file is the user's once more, and a later deletion sticks as before. A PRESENT path is refused: overwriting a file the user owns is `forge project upgrade --force <path>`, which shows the diff first.
Which renderer answers a path, in order:
- The CI mapper (generator.CIWorkflows) for .github/workflows/* and dependabot.yml — THE decision about which workflows a project has. Rendered through `forge generate`'s own CI step, so a rescaffolded workflow is what generate would have written — that step alone, not the whole pipeline, when CI files are all the batch needs generated.
- The rest of the `forge generate` pipeline, for scaffolds it births (the command tree, internal/app, frontend pages, config.k, ...).
- The upgrade renderer, for files `forge project upgrade` manages (Taskfile.yml, Dockerfile, .golangci.yml, ...) and the scaffold-once advisory rows — rendered against the live project.
- The `forge project new` scaffold itself, rendered for this project's forge.yaml into a staging directory (generator.ScaffoldProjectInto) — the only renderer for the DX set, and the one that makes "every file forge scaffolds can be re-emitted" true without a list to maintain.
`forge project scaffolded` — list the scaffold-once files forge has written in this project, and which of them are currently absent.
This is the on-demand half of a trade. `forge generate` used to print the absent set on EVERY run: in control-plane, the same 38 paths plus five lines of explanation, on a clean tree with nothing changed. That is a standing fact rendered as a recurring notice, and a recurring notice is one the reader stops seeing. Generate now reports only the MOMENT the set changes (see generate_missing_scaffold.go); the standing fact moved here, where it is answered when someone asks.
Why a `forge project <noun>` verb rather than a flag on rescaffold: this is a QUESTION about the project, and it sits with the other questions — `forge project map`, `shapes`, `graph`, `features`, `capabilities`. A `--list` flag on rescaffold would put the read behind a write verb, which is both harder to find and easier to run by accident.
Package cli — `forge start`: the greenfield brief, in one call.
WHY THIS EXISTS. A measured dogfood run spent six documentation calls (`forge --help`, `forge project --help`, `forge skill --help`, `forge skill list`, `forge skill load forge`, `forge project new --help`, `forge project annotations`, `forge skill load proto`) before authoring its first proto. Every one of those surfaces is correct and none is hidden; the cost is that reaching the four facts an agent actually needs first — how to lay out the project, how an entity is marked, that custom rpcs are never injected, and how a FK diamond is declared — takes one invocation per surface.
Concatenating those sources is not an option: measured together they run past the consumer's 24 KB output ceiling, so the tail (the pointers onward) would be cut. This command is therefore a CURATION — the pre-first-proto subset, and nothing else.
NOTHING HERE IS TRANSCRIBED. A second hand-written copy of the greenfield prose would drift from the skill that owns it, so every section is composed at run time from the source that already owns it:
- the sequence, the three authoring rules and the shape the embedded skills/forge/SKILL.md, extracted by heading. A retitled or deleted heading is a hard error, never a silent omission.
- the `project new` flags looked up on the real cobra command, so a renamed flag fails loudly instead of advertising one that is gone.
- the proto→column vocabulary scaffold.ProtoSQLMappings(), the same iteration `forge project annotations` publishes.
- the entity markers markerSpecs(), whose names come from the registries the proto scanner spells its markers from.
The deliberate omissions are everything an agent does not need before its first proto: the scaffold tree, the component catalog, deploy, ports, project kinds, and the skill catalog itself. Each is named, not inlined.
Package cli — zero-touch activation of the machine storage policy.
The storage machinery under internal/storage was complete and did nothing, because activation was imperative: `forge storage register` and `forge storage install` had to be run by hand, in order, and never were. A protection that must be switched on is a protection that is off.
This file closes that. Every command that creates or uses a local cache already knows the facts the policy wants, so each one CONVERGES the policy as a side effect of the work it was doing anyway:
ensureDeclaredCluster → contexts + registry container + aliases (cluster phase) registerBuildStorage → repositories + ledger pins + project (forge build)
Neither is allowed to fail the user's command. A policy that cannot be written is a missed cleanup opportunity, not a reason a cluster does not come up, so every path here warns and returns.
Package cli — `forge env up <env>` orchestrator.
One command brings the whole loop up:
- Render the env's KCL → typed entity set.
- Build phase: docker build (per-platform) and push every cluster service / operator / cronjob image; go build each declared build-only variant binary.
- Deploy phase: kubectl apply the cluster manifests + wait operator rollouts + wait one-shot Jobs.
- Host phase: start every host-mode service as a host process, dispatching on deploy.Host.Runner (go-run / air / binary / delve).
- Frontend phase: start every declared frontend in its path dir.
- Wait Ctrl-C → cascade cleanup → exit.
Reaching cluster services from the host is the Gateway API ingress path (see `forge cluster urls`); ad-hoc shells against stateful workloads stay available via `kubectl port-forward` directly.
Replaces the dev-loop bash script every forge project would otherwise hand-write to coordinate build + deploy + run.
Package cli — the machine-local record of which stacks `forge env up` / `forge env up` started, and the teardown that acts on it.
Three files are written per (project, env), under $HOME/.cache/forge/up/<project-id>/:
project.json the project DIRECTORY <project-id> hashes from <env>.pids what this run started: "<name>\t<pid>" per line <env>.env.json the resolved discovery facts (live ports + DATABASE_URL)
The <project-id> segment (see projectIDForDir) is what scopes every record to ONE checkout, so two projects bringing up the same env name never read each other's. Until project.json it was also ALL that was recorded — and a hash cannot be inverted. A stack whose project directory had since been deleted was therefore unreachable by every forge verb: `forge env down` derives the id from the forge.yaml above the working directory, and there was no forge.yaml left to derive it from. Eight rounds of the dogfood loop ended that way, leaving 38 orphaned processes, 7.5 GB resident and 15 listening ports that nothing could name. Recording the path is what makes that set enumerable (`forge env ps`) and stoppable (`forge env down --all`).
These files are a RECORD, never an authority. Teardown proves ownership from the LIVE process markers (up_reclaim.go) and signals nothing else: a ledger drifts — a crashed run never persisted one, air re-execs under a new pid, and a pid the OS has since recycled belongs to a stranger — so "this file remembers pid 4242" is not proof that pid 4242 is ours to kill.
`forge project upgrade`'s scaffold-once report.
The engine — which files are in the tier, and how a difference is classified — lives in internal/generator/upgrade_advisory.go. What lives here is the report, because "here is what you are missing, and here is what taking it would cost you" is a human-facing judgement rather than a generator concern.
The whole section is advisory: nothing in it is written unless the user named that exact path after --force.
`forge project upgrade --check <path>` — the detail half of the report.
The summary (upgrade_report.go) deliberately prints no diffs: at 91 reported files, inline diffs were 14,000 of the 14,244 lines emitted and were the reason the check went unread. Suppressing them is only honest if the diff is one command away, and this file is that command.
It spans BOTH lanes on purpose. The user reading the summary sees one list of paths; making them know whether a path is a Tier-2 managed file or a scaffold-once advisory row before they can ask about it would leak forge's internal tiering into the one gesture that should be trivial. The lane still decides what the footer says, because the remedies really are different — that difference is information, the lookup is not.
The presentation layer shared by both lanes of `forge project upgrade`.
── Why this file exists ──────────────────────────────────────────────
The report was correct and unusable. Run against a mature project (control-plane: two customized Next.js frontends) it emitted 14,244 lines over 91 files, because every reported file dumped its full unified diff inline. Every finding in it was genuine. Nobody reads 14,000 lines, so in practice the check reported nothing — and the two findings that were cheap to act on (.github/CODEOWNERS two lines behind, a newly-reportable eslint.config.mjs) were invisible among the frontend files whose divergence is simply what a customized frontend LOOKS like.
So the split this file implements:
--check SUMMARIZES — what differs, and by how much --check <path> DETAILS — that one file's diff --check --all every row, still without inline diffs
── Ranking is the product ────────────────────────────────────────────
The ordering is not cosmetic; it is the only part of the report that does the reader's triage for them. A pristine file two lines behind is a one-command adopt. A file carrying forty local lines is a merge somebody performs by hand, and forge cannot know which of those lines matter. Presenting the two at equal volume is what buried the cheap ones. So rows are grouped by what adopting them COSTS, cheapest group first, and sorted within a group by the same measure.
── What is capped, and the promise that makes capping honest ─────────
A group prints at most maxRowsPerGroup rows and then says how many it withheld and how to see them. Nothing is dropped from the report: every withheld row is one `--all` away, and every suppressed diff is one `--check <path>` away. Both escape hatches are printed next to the truncation that needs them, because a cap whose escape hatch is documented elsewhere is just a lie with a footnote.
Index ¶
- Constants
- Variables
- func DirectApplyAllowed(e *KCLEntities) bool
- func EnvExists(projectDir, env string) (bool, error)
- func Execute() error
- func ListEnvs(projectDir string) ([]string, error)
- func ListEnvsFromKCLDir(kclDir string) ([]string, error)
- func MergeDescriptorFragments(descriptorOut string) error
- func Name() string
- func NewRootCmd() *cobra.Command
- func RenderProjectMemory(projectRoot string) ([]byte, error)
- func RenderSkillContentAt(projectRoot, skillPath string, audience SkillAudience, cliName string) ([]byte, error)
- func RenderSkillForAudience(body []byte, audience SkillAudience) []byte
- func SetVersion(v, date, commit string)
- func WriteBuildState(projectDir, env string, state BuildState) error
- type AlertDefaultEntity
- type AlertReceiverEntity
- type AlertRouteEntity
- type AlertRuleEntity
- type AlertsEntity
- type AnalyzerSpec
- type AnnotationsSpec
- type ApplyRecord
- type BootstrapSourceEntity
- type BucketRuntime
- type BuildConfigEntity
- type BuildOnlyDeploy
- type BuildState
- type BuildVariant
- type BundleDir
- type CacheRule
- type CapabilitiesSpec
- type ClusterEntity
- type ClusterProvider
- type ClusterRuntime
- type ClusterTargetEntity
- type CodemodReport
- type CommandSpec
- type ComposeRuntime
- type ControlPlaneEntity
- type ControlPlaneSecretEntity
- type DNSRecordEntity
- type DatabaseEntity
- type DockerBuild
- type ExplainEntry
- type ExternalSecretEntity
- type FieldTypeSpec
- type FirebaseRuntime
- type FlowCheckEntity
- type ForgeDescriptor
- type FrontendConfigEntity
- type FrontendEntity
- type FrontendRuntime
- type FrontendTelemetryEntity
- type FunnelEntity
- type GRPCRouteEntity
- type GatewayAddressEntity
- type GatewayEntity
- type GatewayListenerEntity
- type GatewayTLSEntity
- type GenStep
- type GoBuild
- type HTTPRouteEntity
- type HandlerPath
- type HelmChartEntity
- type HostInfraEntity
- type HostRuntime
- type HostedRuntime
- type IngressURL
- type KCLEntities
- type KCLEnvVar
- type KubeconfigPolicyRuleEntity
- type KubeconfigSecretEntity
- type KubeconfigServiceAccountEntity
- type LibrariesSpec
- type LibraryDivergence
- type LibrarySpec
- type LibrarySymbol
- type LiveDrift
- type LiveDriftObject
- type LiveEnvironment
- type ManifestClusterEntity
- type MapNode
- type MarkerSpec
- type ObservabilityDestEntity
- type ObservabilityEntity
- type ObservabilityPolicyEntity
- type ObservabilitySignalsEntity
- type OpenBaoBootstrapEntity
- type OpenBaoEntity
- type OptionSpec
- type RemoteBuild
- type RemoteBuildSource
- type RenderedSecretEntity
- type RenderedSecretKeyEntity
- type RouteHealthCheckEntity
- type RouteOutlierDetectionEntity
- type RouteRetriesEntity
- type RouteTrafficEntity
- type RuntimeEntity
- type SecretProviderEntity
- type ShellBuild
- type SkillAudience
- type SkillEmit
- type SkillListOptions
- type SkillMetaPublic
- type SkillRelevance
- type SkillScope
- type StaticSiteCDN
- type StoreSourceEntity
- type ValidateRuleSpec
- type WebRuntimeModule
- type WebRuntimeSpec
- type WorkloadEntity
- func (w WorkloadEntity) EffectiveBuildCmd() string
- func (w WorkloadEntity) EffectiveBuildCwd() string
- func (w WorkloadEntity) EffectiveBuildEnv() map[string]string
- func (w WorkloadEntity) EffectiveBuiltImages() map[string]string
- func (w WorkloadEntity) EnvVars() []KCLEnvVar
- func (w WorkloadEntity) GoBuild() *GoBuild
- func (w WorkloadEntity) HostEnv() map[string]string
- func (w WorkloadEntity) HostPort() int
- func (w WorkloadEntity) HostPorts() []int
- func (w WorkloadEntity) IsJob() bool
- func (w WorkloadEntity) LongRunning() bool
- func (w WorkloadEntity) OnRuntime(runtime string) bool
- func (w WorkloadEntity) PinnedBuildTag() (string, bool)
Constants ¶
const ( // FrontendRuntimeHost is the dev server (`<dev_runner> dev`). A deploy // ships nothing for it. FrontendRuntimeHost = "host" // FrontendRuntimeHosted is platform static hosting: a release artifact // plus a StaticSite CR published to the control plane. FrontendRuntimeHosted = "hosted" // FrontendRuntimeBucket is the author's own object-storage bucket. FrontendRuntimeBucket = "bucket" // FrontendRuntimeFirebase is Firebase Hosting. FrontendRuntimeFirebase = "firebase" // FrontendRuntimeBuildOnly is built by a deploy, never shipped: its // output exists for a sibling frontend's `bundle`. FrontendRuntimeBuildOnly = "build-only" )
Frontend runtime discriminators (FrontendRuntime.Type). The dispatch table is keyed on exactly these; a typo at one call site would silently skip a frontend, so every site names the constant.
const ( RuntimeHost = "host" RuntimeCompose = "compose" RuntimeCluster = "cluster" RuntimeHosted = "hosted" RuntimeBuildOnly = "build-only" )
Runtime discriminators (RuntimeEntity.Type).
const ( OwnershipUser = "user-owned" OwnershipForgeSpace = "forge-space, regenerated" OwnershipForgeDrifted = "forge-space, hand-edited (drift from regen)" OwnershipScaffold = "scaffold, FORGE_SCAFFOLD markers present" OwnershipScaffoldOnce = "user-owned, scaffolded once" OwnershipUnknown = "" )
Ownership classes used in the output. We pick a narrow set so a downstream agent can switch on them.
const ( // QueryModeStructured marks a statement the compiler built from typed flags. QueryModeStructured = "structured" // QueryModeRaw marks a statement the caller wrote (--sql, or a panel's sql). QueryModeRaw = "raw" )
const BeforeAll = "*"
BeforeAll is the BROADCAST selector for a one-shot's `before`: "gate every workload in this environment", as opposed to an enumerated list of dependent names. It mirrors `forge.workloads.BEFORE_ALL` in the KCL layer, which is where the value is defined and documented.
A workload can never be NAMED this — a workload name is used verbatim as a Kubernetes object name and "*" is not a legal DNS-1123 label — so the wildcard cannot collide with a real dependent.
const DefaultHostedPlatform = "amd64"
DefaultHostedPlatform is the GOARCH of the reliant platform's nodes, the value forge.OnHosted.platform defaults to in KCL. It is repeated here only for entities built without a render (the KCL always supplies it).
const MetricsSchemaArtifact = "observability/metrics-schema.json"
MetricsSchemaArtifact is the schema's path relative to the build output directory, and its path inside a release bundle.
const ReasonNoControlPlaneCredential = "no_control_plane_credential"
ReasonNoControlPlaneCredential is the error_reason of a failure to RESOLVE a credential (cloud.ErrNoCredential): nothing was presented, so nothing was refused. Signing in fixes it.
Variables ¶
var ErrProjectConfigNotFound = cmdutil.ErrProjectConfigNotFound
ErrProjectConfigNotFound is returned when forge.yaml does not exist. The canonical sentinel lives in cmdutil (the shared leaf package) so the dir-nested command groups compare against the same value; this is an alias.
Functions ¶
func DirectApplyAllowed ¶ added in v0.1.45
func DirectApplyAllowed(e *KCLEntities) bool
DirectApplyAllowed reports whether forge may apply to this environment DIRECTLY, rather than recording a bundle for a reconciler to converge.
True in two cases, and they are different reasons:
- the env DECLARES `lifecycle = "local" | "ephemeral"` — it is a cluster forge creates and can delete, so applying to it is the intended path. KCL has already checked that claim against every cluster the env targets, so a GKE context cannot reach here by declaring itself local;
- the env TARGETS NO CLUSTER AT ALL. A host-only or compose-only env has nothing to apply, so "may forge apply directly" is vacuously true. Answering false here would describe such an env as needing a reconciler, which is the opposite of true: there is no cluster for one to reconcile against.
An env that declares nothing AND targets a cluster is a REAL environment, and this returns false for it. That default is deliberate: an env whose author forgot to declare it is treated as production, not as scratch.
- the env DECLARES who applies it: `applier = "forge"`, or `hub_converges = [...]` for the units forge still applies. The cluster is not scratch (lifecycle would be a false claim, and KCL checks it), and the declaration is the env saying it is mid-move onto the hub, so direct apply is allowed for now. It is a transitional allowance: it shrinks as units move, and ends when the list names every unit.
TODAY NOTHING REFUSES ON THIS. It drives a notice and the JSON projections. The refusal (F-REFUSE-DIRECT) will land for an env that declares none of the three, and because the predicate is already here and already correct, that change is a flag flip rather than a migration.
func EnvExists ¶
EnvExists reports whether `deploy/kcl/<env>/main.k` exists. Returns false (with no error) for the absent-env / missing-kcl-dir cases — callers typically convert that into a friendly "env not configured" error themselves.
func Execute ¶
func Execute() error
Execute is the entrypoint used by main() to dispatch the assembled root cobra command. Wraps NewRootCmd().Execute().
pgtest.Shutdown releases this process's share of the cross-process embedded postgres pool (a no-op unless a command — `forge generate` and the scaffold verbs are the ones that reach it — actually booted or reused one). Deferred HERE, before main()'s os.Exit, so a failed command still releases its share and the last process out tears the shared server down cleanly instead of leaking its data dir and SysV IPC.
func ListEnvs ¶
ListEnvs returns the names of every environment declared via a `deploy/kcl/<env>/main.k` file. The list is sorted alphabetically for deterministic output. An absent `deploy/kcl/` directory yields an empty list (and no error) — that's the shape of a brand-new project, not a problem.
projectDir is the project root (the directory containing forge.yaml). Callers can pass either projectDir or kclDir directly — see ListEnvsFromKCLDir for the lower-level variant.
func ListEnvsFromKCLDir ¶
ListEnvsFromKCLDir is the lower-level discovery walker. It exists so callers that already have the kcl root (e.g. a forge.yaml- configured cfg.K8s.KCLDir) can skip the projectDir join.
func MergeDescriptorFragments ¶
MergeDescriptorFragments combines all per-invocation fragments under <descriptorOut>/<descriptorStageDir>/ into a single forge_descriptor.json at <descriptorOut>/forge_descriptor.json, then removes the staging dir. Called by runDescriptorGenerate in the parent process after buf returns.
Idempotent: running it twice is safe (the second run sees an empty stage dir and is a no-op apart from rewriting the final descriptor with what's already there). Returns nil silently when no fragments exist (clean projects with no services/entities/configs).
func Name ¶
func Name() string
Name returns the command name users should type to invoke Forge. When the binary is "forge" (standalone install), it returns "forge". When embedded in another binary (e.g. "reliant"), it returns "reliant forge". Forwards to cmdutil.Name so the dir-nested command groups share one implementation without importing internal/cli.
func NewRootCmd ¶
NewRootCmd builds and returns the fully assembled root command.
func RenderProjectMemory ¶
RenderProjectMemory reads forge.yaml at projectRoot, renders the project memory template with the project name + invoking CLI name — as a static site's memory for a site (no go.mod, the app at the root) — and returns the bytes. Used by out-of-process consumers (the reliant CLI) to inject framework context in-memory rather than reading a possibly-stale on-disk reliant.md.
The template body is the same one `forge project new` writes for non-reliant harnesses, so in-memory and on-disk renderings stay byte-identical.
Returns an error when forge.yaml is missing, unreadable, has no `name:` field, or when the template fails to render.
func RenderSkillContentAt ¶ added in v0.1.45
func RenderSkillContentAt(projectRoot, skillPath string, audience SkillAudience, cliName string) ([]byte, error)
RenderSkillContentAt returns a skill exactly as `forge skill load` prints it: resolved with user > project > forge precedence, the version-skew advisory inserted (ResolveSkillContentAt), filtered for audience, and every bare `forge ` command reference rewritten to cliName.
It is the ONE renderer behind both `skill load` and the public cli.RenderSkill. A harness that injects a skill into an agent's context used to get the raw body from cli.LoadSkill while the CLI printed a rewritten one, so an agent working under `reliant forge` was handed `forge generate` — a command it may not have, or worse, a different forge build than the one that generated its project.
A "forge/" prefix is accepted, matching `skill load`.
func RenderSkillForAudience ¶
func RenderSkillForAudience(body []byte, audience SkillAudience) []byte
RenderSkillForAudience returns the skill body filtered for the given audience. For SkillAudienceGeneral, `<!-- @forge-only:start -->` ... `<!-- @forge-only:end -->` blocks are removed (markers included). For every other audience the body is returned unchanged.
Exported so out-of-process consumers (e.g. reliant) can apply the same filtering rule when serving a skill loaded via [LoadSkill]. Forge-side callers can use it directly.
func SetVersion ¶
func SetVersion(v, date, commit string)
SetVersion stamps the version/date/commit metadata used by the `version` subcommand and the rendered Cobra Version string. Called once from main() with ldflags-injected values.
func WriteBuildState ¶
func WriteBuildState(projectDir, env string, state BuildState) error
WriteBuildState persists a successful `forge env build --push` to disk. Called by runBuild after every per-image push succeeds, so the most recent push is always the source of truth a subsequent `forge env deploy <env>` consumes.
The directory is created lazily — projects that never use --push never grow a .forge/state/ tree. File is 0o644 (world-readable) to match the other .forge state files' mode; nothing in here is secret.
Types ¶
type AlertDefaultEntity ¶ added in v0.1.45
type AlertDefaultEntity struct {
Enabled bool `json:"enabled"`
Labels map[string]string `json:"labels"`
For string `json:"for,omitempty"`
Params map[string]float64 `json:"params"`
}
AlertDefaultEntity is one forge.AlertDefault.
type AlertReceiverEntity ¶ added in v1.0.0
type AlertReceiverEntity struct {
Name string `json:"name"`
Type string `json:"type"`
URL string `json:"url,omitempty"`
Secret string `json:"secret,omitempty"`
Channel string `json:"channel,omitempty"`
Severities []string `json:"severities"`
AlwaysAlertnames []string `json:"always_alertnames"`
}
AlertReceiverEntity is one forge.AlertReceiver.
type AlertRouteEntity ¶ added in v0.1.45
type AlertRouteEntity struct {
Match map[string]string `json:"match"`
Receiver string `json:"receiver"`
Continue bool `json:"continue"`
}
AlertRouteEntity is one forge.AlertRoute.
type AlertRuleEntity ¶ added in v0.1.45
type AlertRuleEntity struct {
Alert string `json:"alert"`
Expr string `json:"expr"`
For string `json:"for"`
Interval string `json:"interval"`
KeepFiringFor string `json:"keep_firing_for,omitempty"`
Labels map[string]string `json:"labels"`
Annotations map[string]string `json:"annotations"`
Enabled bool `json:"enabled"`
}
AlertRuleEntity is one forge.AlertRule, severity already defaulted.
type AlertsEntity ¶ added in v0.1.45
type AlertsEntity struct {
Rules []AlertRuleEntity `json:"rules"`
Defaults map[string]AlertDefaultEntity `json:"defaults"`
Routes []AlertRouteEntity `json:"routes"`
// Receivers are the local stack's (forge.AlertReceiver); a hosted env's
// receivers live in the control plane.
Receivers []AlertReceiverEntity `json:"receivers"`
}
AlertsEntity mirrors kcl/schema.k Observability.alerts as render.k projects it.
type AnalyzerSpec ¶ added in v0.1.45
type AnalyzerSpec struct {
Flag string `json:"flag"`
Usage string `json:"usage"`
Hidden bool `json:"hidden"`
}
AnalyzerSpec is one `forge lint` analyzer flag. Hidden reports whether it is a maintainer flag suppressed from `forge lint --help` (still fully functional, listed by `forge lint --help-dev`).
type AnnotationsSpec ¶ added in v0.1.45
type AnnotationsSpec struct {
Markers []MarkerSpec `json:"markers,omitempty"`
FieldTypes []FieldTypeSpec `json:"field_types,omitempty"`
ValidateRules []ValidateRuleSpec `json:"validate_rules,omitempty"`
ServiceOptions []OptionSpec `json:"service_options,omitempty"`
MethodOptions []OptionSpec `json:"method_options,omitempty"`
}
AnnotationsSpec is the whole dump. Sections are omitempty so a `--kind` filter yields only the selected section(s).
type ApplyRecord ¶ added in v0.1.45
type ApplyRecord struct {
Apply release.Apply
Outcome *release.ApplyOutcome
// State is release.DeriveApplyState's answer. Carried rather than
// recomputed by each caller so "abandoned" is drawn at one place.
State release.ApplyState
}
ApplyRecord is one convergence record as a READER sees it: what was attempted, its outcome if one arrived, and the state derived from both plus the clock.
type BootstrapSourceEntity ¶ added in v1.0.0
type BootstrapSourceEntity struct {
Type string `json:"type"`
Project string `json:"project,omitempty"`
}
BootstrapSourceEntity is the rendered Bundle.bootstrap_source.
type BucketRuntime ¶ added in v0.1.45
type BucketRuntime struct {
Bucket string `json:"bucket"`
CDN *StaticSiteCDN `json:"cdn,omitempty"`
// KeepReleases has NO omitempty, deliberately. 0 is a meaningful
// value ("retain everything, never prune"), and the KCL renderer
// projects the key unconditionally so it survives the round trip. An
// omitempty here would drop it and the provider would read the
// absent key back as the default 10, pruning archived releases the
// author explicitly asked forge to keep — in a bucket, where the
// loss is not recoverable.
KeepReleases int `json:"keep_releases"`
}
BucketRuntime mirrors forge.OnBucket: WHERE the assembled tree goes. The build and assembly inputs (public_dir, base_path, bundle, cache_control) are the frontend's own (FrontendEntity).
type BuildConfigEntity ¶
type BuildConfigEntity struct {
Unit string
InputsFile string
Type string // "go" | "docker" | "shell" | "remote" | "" (absent)
Go *GoBuild // populated when Type=="go"
Docker *DockerBuild // populated when Type=="docker"
Shell *ShellBuild // populated when Type=="shell"
Remote *RemoteBuild // populated when Type=="remote"
}
BuildConfigEntity is the dispatched-by-type view of a workload's build block. The raw JSON is a tagged union; Type carries the tag; exactly one of Go/Docker/Shell/Remote is non-nil after [dispatchBuild] runs. Type=="" means the KCL `build` block was absent (null): forge does not build the workload, and nothing is synthesized.
type BuildOnlyDeploy ¶
type BuildOnlyDeploy struct {
BuildVariants []BuildVariant `json:"build_variants,omitempty"`
}
BuildOnlyDeploy is the forge.BuildOnly runtime: the workload is built and shipped as an artifact, never run — a CLI shipped in a release, etc. BuildVariants lets one workload emit several binaries (different ldflags / build tags).
type BuildState ¶
type BuildState struct {
// Image is the REPOSITORY this build wrote, registry host included
// (`ghcr.io/acme/shop`) — the reference the workload declared, minus any
// tag. There is no separate Registry field: the registry is part of the
// image, so splitting them here would let the two disagree, which is the
// class of bug that made a push and a deploy name different repositories.
Image string `json:"image"`
Tag string `json:"tag"`
// Pushed is true when the image was pushed to that repository. False for
// local/scp/compose builds — the image lives only on the build host.
// Recording the handoff no longer depends on a push (that gate left
// non-registry deploys with no tag to read).
Pushed bool `json:"pushed"`
// Git provenance of the build, so `forge env deploy` can warn when it's
// about to ship a non-reproducible (dirty / untagged) build. Commit
// is the full HEAD sha; GitTag is the exact tag on HEAD (empty when
// HEAD isn't tagged); Dirty is true when the working tree had
// uncommitted changes at build time.
Commit string `json:"commit,omitempty"`
GitTag string `json:"git_tag,omitempty"`
Dirty bool `json:"dirty,omitempty"`
// PushedAt is the wall-clock build time, formatted as time.RFC3339.
// The state file is informational across forge invocations, so we use
// real time here — reproducibility constraints don't apply.
PushedAt string `json:"pushed_at"`
// Digest is the content-addressed manifest digest of the pushed image,
// in the canonical `sha256:...` form (no `@` prefix, no repo). Captured
// from the registry after `docker push` succeeds (see imageRepoDigest).
// EMPTY for non-pushed builds (local/scp/compose — the image lives only
// on the build host with no registry manifest to address) and for any
// build where the digest lookup failed (capture is best-effort and never
// fails the build).
//
// When present, `forge env deploy` pins the manifest to `<image>@<Digest>`
// instead of the mutable `:Tag` — a digest can't go stale and can't be
// re-pointed, so the node-cache / re-tag-didn't-take failure class is
// structurally impossible. When empty, deploy falls back to the tag, so
// every non-pushed transport keeps working unchanged.
Digest string `json:"digest,omitempty"`
// Platforms is the set of OS/arch platforms the pushed image advertises
// (e.g. ["linux/amd64"], or both for a multi-arch index), captured from
// the registry manifest alongside Digest. Informational today (a human
// can eyeball what arch shipped); the deploy preflight inspects the live
// image's arch independently. Empty when the lookup failed or the build
// wasn't pushed.
Platforms []string `json:"platforms,omitempty"`
}
BuildState records what `forge env build --push` actually pushed to a registry, so a subsequent `forge env deploy <env>` can reference the same tag even when the working tree has changed between phases.
The original bug this struct closes: `forge build` tags an image `<reg>/<svc>:<git-describe>` (which includes `-dirty` when the working tree has untracked or modified files), then `forge env deploy` independently computes a tag — and the two diverge whenever a working-tree mutation between the two phases flips the dirty bit, or when the two phases use different git commands altogether (build uses `git describe --tags --always --dirty`, deploy used `git rev-parse --short HEAD`). The state file fixes both by making build authoritative.
Wire format is JSON; fields use snake_case for readability when a user peeks at the file by hand. PushedAt is RFC3339 so a human can eyeball "how stale is this?" without a parser.
func ReadBuildState ¶
func ReadBuildState(projectDir, env string) (*BuildState, error)
ReadBuildState loads the per-env build-state file. Returns (nil, nil) when the file is missing — that's the "deploy-without-build" path (CI with a separate build job, or the user running `forge env deploy` on a fresh checkout) and the caller falls through to resolveImageTag. Returns (nil, err) for malformed JSON or unreadable files; callers should not silently swallow these because they mean the state file exists but can't be trusted.
type BuildVariant ¶
type BuildVariant struct {
Name string `json:"name"`
Ldflags []string `json:"ldflags,omitempty"`
BuildTags []string `json:"build_tags,omitempty"`
GOOS string `json:"goos,omitempty"`
GOARCH string `json:"goarch,omitempty"`
EnvAtBuild map[string]string `json:"env_at_build,omitempty"`
OutputName string `json:"output_name,omitempty"` // default: <service>-<variant>
}
BuildVariant describes one binary produced by a build-only service.
type BundleDir ¶ added in v0.1.45
BundleDir is one extra pre-built static directory assembled into a published site alongside the frontend's own build output. Dest empty means the site root. Runtime-neutral: every static runtime assembles it the same way.
type CacheRule ¶ added in v0.1.45
type CacheRule struct {
Pattern string `json:"pattern"`
CacheControl string `json:"cache_control"`
}
CacheRule is one Cache-Control header applied to the objects a glob matches. Order is significant: first match wins.
type CapabilitiesSpec ¶ added in v0.1.45
type CapabilitiesSpec struct {
Commands []CommandSpec `json:"commands,omitempty"`
Analyzers []AnalyzerSpec `json:"analyzers,omitempty"`
Markers []MarkerSpec `json:"markers,omitempty"`
}
CapabilitiesSpec is the whole dump.
type ClusterEntity ¶
type ClusterEntity struct {
Name string `json:"name"`
// Provider selects which ClusterProvider (see cluster_provider.go)
// ensures this cluster: "k3d" (default), "vcluster", or "gke". Only
// "k3d" is implemented — see clusterProviderRegistry.
Provider string `json:"provider,omitempty"`
// Context is the derived kubectl context (`k3d-<name>`), projected so
// the reconcile / kubeconfig mint can target the cluster without
// re-deriving the prefix.
Context string `json:"context,omitempty"`
Config string `json:"config,omitempty"`
// Image pins the k3s node image used for flag-driven clusters. Config-driven
// clusters declare the image in their authoritative k3d YAML instead.
Image string `json:"image,omitempty"`
// Network is the derived docker network this cluster joins —
// `k3d-<owner.name>` for a secondary, empty for an owner cluster.
Network string `json:"network,omitempty"`
// RegistryInherit is the derived registry-inherit flag — true when an
// `owner` is set (forge mirrors the owner's registry onto this
// cluster's node), false for an owner cluster.
RegistryInherit bool `json:"registry_inherit,omitempty"`
Servers int `json:"servers,omitempty"`
Agents int `json:"agents,omitempty"`
APIPort int `json:"api_port,omitempty"`
// Ingress, when true, installs the Gateway API stack (pinned Gateway-API
// CRDs + the Envoy Gateway controller via helm + the `eg` GatewayClass)
// into this cluster after it's ensured. A fresh k3d cluster ships none of
// these; an env whose Gateway/HTTPRoute/GRPCRoute resources land on this
// cluster needs it on. Idempotent (helm upgrade --install + kubectl apply).
//
// This is the IMPERATIVE install. An env that declares its Gateway API
// controller DECLARATIVELY as a forge.HelmChart platform dep
// (Bundle.helm_charts) leaves Ingress false and sets HostPorts true.
Ingress bool `json:"ingress,omitempty"`
// HostPorts, when true, merges the generated deploy/k3d-ports.yaml Gateway
// listener host-port fragment into the k3d config at create time. Ingress
// IMPLIES this (an imperatively-installed Gateway also needs the host
// ports). Set HostPorts explicitly when the controller is installed
// declaratively (Ingress=false) but the cluster still hosts a Gateway
// whose listeners must be host-mapped at create time.
HostPorts bool `json:"host_ports,omitempty"`
// ClusterCIDR / ServiceCIDR are the pod and Service address blocks k3s
// allocates from, reaching `k3d cluster create` as the k3s server args
// --cluster-cidr / --service-cidr. Empty (the default, and every cluster
// that does not declare them) leaves k3s on its own defaults —
// 10.42.0.0/16 pods, 10.43.0.0/16 services — so no argument is added and
// the created cluster is identical to before.
//
// They exist because two k3d clusters on ONE docker network both default
// to the SAME pod CIDR, so the same pod IP exists in both and a local
// route always wins: a workload in one cluster cannot dial a pod IP in the
// other. Disjoint blocks are what make that cross-cluster pod dialing
// possible, which is how the equivalent cloud topology (several clusters
// in one VPC, on disjoint blocks) already works.
//
// Validated KCL-side (shape, and that a cluster's two blocks do not
// overlap each other) — see kcl/schema.k's Cluster check block.
ClusterCIDR string `json:"cluster_cidr,omitempty"`
ServiceCIDR string `json:"service_cidr,omitempty"`
}
ClusterEntity mirrors the kcl/schema.k Cluster — a k3d cluster forge ensures exists before deploying. The reconcile (clusterPhase) reads these and runs `k3d cluster create` for any that are absent.
Ownership is a REFERENCE: a secondary cluster names its `owner` Cluster; the KCL render layer DERIVES the joined network (`k3d-<owner.name>`, projected into Network) and the registry-inherit behavior (projected into RegistryInherit=true). An owner cluster projects an empty Network and RegistryInherit=false. There is no "primary" field and no most-X heuristic.
type ClusterProvider ¶ added in v0.1.45
type ClusterProvider interface {
// Ensure creates the entity's cluster if absent, starts it if stopped,
// and heals known-flaky runtime state if already running — then mints
// (or verifies) its kubectl context per the ordering contract above.
// declared is the full cluster list for this env, passed through
// because ordering across DECLARED clusters matters (a secondary must
// be ensured after the owner it nests on; see isNestedSecondary).
Ensure(ctx context.Context, entity ClusterEntity, declared []ClusterEntity, projectDir, env string) error
}
ClusterProvider ensures ONE declared cluster exists and is healthy enough to receive a deploy. It is the cluster-provisioning analogue of deploytarget.Provider — see internal/deploytarget/deploytarget.go — which plays the same dispatch role one layer up, per SERVICE rather than per CLUSTER.
Deliberately ONE method. Every current caller (reconcileDeclaredClusters, prepareDeployCluster) asks exactly one question — "does this cluster exist and is it ready" — and the create/start/heal distinction k3d needs internally is an implementation detail of ITS provider, not a decision any caller of the registry makes. Adding a second method here (e.g. a separate Start or a Teardown) would be modelling k3d's own ordering in the interface rather than what the registry's callers need; per the project's package-boundary rule, a six-method interface is a concrete type in disguise, and a two-method one is already worth scrutinizing.
CONTEXT-MINTING ORDERING — the reason Ensure carries this contract rather than just "create the cluster":
verifyDeclaredContextsExist (deploy.go) is the guard that refuses a deploy whose declared kubectl context isn't present in the local kubeconfig — the mechanism that makes a wrong-cluster deploy impossible. It runs in the DEPLOY phase, strictly after the CLUSTER phase (reconcileDeclaredClusters) has already returned — see upBuildDeployPhases (up.go) and prepareDeployCluster→ resolveDeployKubectlContext (deploy.go), both of which call the cluster phase before ever building the deploy groups the context guard reads.
For k3d this ordering is invisible because k3d mints the context as a side effect of the SAME command that creates the cluster: createK3dCluster's `k3d cluster create` writes the `k3d-<name>` context into the default kubeconfig, and mergeK3dKubeconfigFn repairs that merge on the hung-client recovery path. By the time reconcileDeclaredClusters returns, `.context` already resolves — so the ordering guarantee below has always held for k3d, just never had to be written down because nothing violated it.
A non-k3d provider does NOT get this for free. A vcluster's kubeconfig context exists only after `vcluster connect` (or equivalent) writes it; a freshly-created GKE cluster's context exists only after `gcloud container clusters get-credentials` does the same. If a future ClusterProvider deferred that mint to the deploy phase, or skipped it, verifyDeclaredContextsExist would still fire — correctly, the context really is missing — but the operator reads a kubeconfig error at deploy time for what is actually an incomplete cluster-provisioning step. That is a strictly worse failure to debug than the same problem surfacing where it happened.
So the contract every ClusterProvider.Ensure implementation MUST uphold: by the time Ensure returns nil, entity.Context (the kubectl context string forge derived for this cluster) is resolvable in the LOCAL kubeconfig. Ensure runs entirely inside the cluster phase, which always completes before the deploy phase's context guard runs — so upholding the contract here is what keeps a missing-context failure attributable to cluster provisioning, not to deploy.
type ClusterRuntime ¶ added in v0.1.45
type ClusterRuntime struct {
Cluster string `json:"cluster,omitempty"`
Namespace string `json:"namespace,omitempty"`
ConnectedCluster string `json:"connected_cluster,omitempty"`
Registry string `json:"registry,omitempty"`
Domain string `json:"domain,omitempty"`
Platform string `json:"platform,omitempty"` // GOARCH override; empty = forge.yaml deploy.target_arch
ImagePullSecrets []string `json:"image_pull_secrets,omitempty"`
}
ClusterRuntime is forge.Cluster: a Kubernetes cluster the author operates. Cluster IS the kubectl context. Replicas, ports and probes are not here — they are the workload's spec, the same on every runtime.
type ClusterTargetEntity ¶ added in v0.1.45
type ClusterTargetEntity struct {
Cluster string `json:"cluster,omitempty"`
Namespace string `json:"namespace,omitempty"`
Registry string `json:"registry,omitempty"`
Domain string `json:"domain,omitempty"`
Platform string `json:"platform,omitempty"`
// ConnectedCluster is the control-plane cluster this target names, by
// the name `forge cluster connect` registered. Empty for a k3d cluster,
// which forge applies to directly.
ConnectedCluster string `json:"connected_cluster,omitempty"`
}
ClusterTargetEntity is the rendered `cluster_target` (kcl/render.k `_render_cluster_target`): the env-wide coordinates a Bundle declares once. Only the fields forge resolves env-wide facts from are read.
type CodemodReport ¶
CodemodReport is what the codemods an upgrade ran rewrote, merged in release order.
type CommandSpec ¶ added in v0.1.45
type CommandSpec struct {
Path string `json:"path"` // e.g. "forge scaffold entity"
Short string `json:"short"` // the command's own Short
Group bool `json:"group"` // true when it only groups subcommands
}
CommandSpec is one node of the command tree: the full invocation path with its own one-line summary.
type ComposeRuntime ¶ added in v0.1.45
type ComposeRuntime struct {
Service string `json:"service,omitempty"` // default: the workload name
File string `json:"file,omitempty"` // default docker-compose.yml
EnvFile string `json:"env_file,omitempty"`
Wait *bool `json:"wait,omitempty"`
WaitTimeout int `json:"wait_timeout,omitempty"`
// Env is the map forge puts in the `docker compose` PROCESS environment
// — where the compose file's own `${VAR}` references interpolate from.
// Distinct from EnvFile, which only reaches containers.
Env map[string]string `json:"env,omitempty"`
// the repo uses; forge drives it from the primary checkout. See
// OnCompose.shared in kcl/workload.k.
Shared bool `json:"shared,omitempty"`
// Optional marks a service the app does not need in order to run (local
// observability): `forge env up` skips it, with a warning, when no Docker
// daemon answers. See OnCompose.optional in kcl/workload.k.
Optional bool `json:"optional,omitempty"`
}
ComposeRuntime is forge.Compose: the workload is a docker-compose service. Compose owns the container definition, so the workload contributes only its name and this block.
Wait is a pointer so "absent from the JSON" is distinguishable from "explicitly False": the absent case must resolve to the safe default (wait) rather than to the bool zero value.
type ControlPlaneEntity ¶ added in v0.1.45
type ControlPlaneEntity struct {
Type string `json:"type"`
Endpoint string `json:"endpoint"`
TokenEnv string `json:"token_env,omitempty"`
// RegistryHost is the platform registry this org's artifacts live on,
// defaulted in KCL to forge.RELIANT_REGISTRY_HOST.
//
// This is NOT the registry a workload's image names — that one is still
// the author's, still on the workload, and used verbatim. This is the
// subtree a BARE hosted image is composed under, which is the one case
// the author does not choose because the platform admits exactly one.
RegistryHost string `json:"registry_host,omitempty"`
// Claimable declares the env an ANONYMOUS deploy (`forge env deploy
// <env> --claimable`): it is judged under the claimable plan
// (deploy.HostedPlanClaimable) by every preview and check, and a
// signed-in deploy of it is refused until the declaration is removed.
Claimable bool `json:"claimable,omitempty"`
// contains filtered or unexported fields
}
ControlPlaneEntity is the parsed bundle-level hosted control-plane declaration (kcl/schema.k ControlPlane): WHICH endpoint this env talks to, and the NAME of the env var its bearer credential is read from.
It carries no credential and never will. The token VALUE is resolved Go-side by internal/cloud from the flag / env var / login file, exactly as secret VALUES are resolved by internal/secrets rather than KCL.
type ControlPlaneSecretEntity ¶ added in v0.1.45
type ControlPlaneSecretEntity struct {
Name string `json:"name"`
Namespace string `json:"namespace"`
// Cluster is the kube context it lands in; ConnectedCluster is the name
// `forge cluster connect` registered, the credential it is delivered with.
Cluster string `json:"cluster"`
ConnectedCluster string `json:"connected_cluster,omitempty"`
// Kind says what the control plane mints into it ("telemetry_token",
// "identity", "identity_machine_key", "identity_jwks").
Kind string `json:"kind"`
// Keys are the data keys the control plane writes.
Keys []string `json:"keys,omitempty"`
Reason string `json:"reason,omitempty"`
// Required is true when a workload reads the Secret without an optional
// marker, so its pods wait for the first delivery.
Required bool `json:"required,omitempty"`
// Delivery is release.ControlPlaneSecretDeliveryStore when the control
// plane writes the store and ESO delivers the Secret; Source is then the
// floating store source ESO reads.
Delivery string `json:"delivery,omitempty"`
Source *StoreSourceEntity `json:"source,omitempty"`
}
ControlPlaneSecretEntity mirrors the kcl/schema.k ControlPlaneSecret — a Secret the env's declared control plane supplies and renews. It carries a destination and a kind, never a value.
Unlike an ExternalSecret it is NOT a precondition of a deploy: the control plane delivers it after one, so the preflight neither checks that it exists nor blocks on it. Recorded in the env shape so the control plane knows what to deliver where.
type DNSRecordEntity ¶
type DNSRecordEntity struct {
Host string `json:"host"`
Type string `json:"type"`
// Target is the expected value (LB IP for A/AAAA, CNAME target). Often
// unknown at author time (ephemeral LB IP), so optional.
Target string `json:"target,omitempty"`
Reason string `json:"reason,omitempty"`
}
DNSRecordEntity mirrors the kcl/schema.k DNSRecord — a DNS record a deploy depends on. forge can't authoritatively verify external DNS, so this is a render-time checklist entry (and a `--check` note), not a hard block. Target is the expected value (LB IP / CNAME target) when known.
type DatabaseEntity ¶ added in v0.1.45
type DatabaseEntity struct {
Name string `json:"name"`
Runtime string `json:"runtime,omitempty"`
Cluster string `json:"cluster,omitempty"`
Namespace string `json:"namespace,omitempty"`
Spec deployv1alpha1.ManagedDatabaseSpec `json:"spec"`
}
DatabaseEntity is one managed database: its name (the CloudNativePG Cluster name), its runtime ("cluster" or "hosted"), where it lands when cluster-bound, and its spec, which is forge's Go tier type.
func (DatabaseEntity) Hosted ¶ added in v0.1.45
func (d DatabaseEntity) Hosted() bool
Hosted reports whether the control plane runs this database.
type DockerBuild ¶
type DockerBuild struct {
OutputName string `json:"output_name,omitempty"`
Dockerfile string `json:"dockerfile,omitempty"`
// Context is the MAIN `docker build` context directory, relative to the
// project root. Empty is the project root ("."), so an unset Context
// builds byte-identically to before this field existed. Distinct from
// BuildContexts, which are NAMED --build-context entries. The Dockerfile
// need not live under it — docker allows -f outside the context.
Context string `json:"context,omitempty"`
Platform string `json:"platform,omitempty"`
Target string `json:"target,omitempty"`
BuildArgs map[string]string `json:"build_args,omitempty"`
// BuildContexts maps a `docker buildx --build-context name=value` entry
// THIS service's Dockerfile needs (a sibling-checkout path the Dockerfile
// `COPY --from=name`s, a `docker-image://` override, …). Same value shapes
// as config.DockerConfig.BuildContexts. Empty falls back to the
// project-level forge.yaml docker.build_contexts.
BuildContexts map[string]config.BuildContext `json:"build_contexts,omitempty"`
}
DockerBuild mirrors the kcl/schema.k DockerBuild — the per-service container image build. Reuses forge's existing docker primitives (tag/registry/push/build-contexts) as behavior; these fields select the dockerfile/platform/target/build_args.
Registry + BuildContexts are the per-service `docker` facts that are NOT expressible in a Dockerfile — the push target for THIS service's image and the named build contexts THIS service's Dockerfile `COPY --from=`s. Each falls back to the project-level forge.yaml `docker.{registry,build_contexts}` when unset, so a single-image project keeps declaring them once at the top level. KCL renders per env, so these are per-service AND per-env.
type ExplainEntry ¶
type ExplainEntry struct {
OutputPath string `json:"output_path"`
Sources []string `json:"sources,omitempty"`
Kind string `json:"kind"` // "service-handler", "service-mock", "entity-orm", "config", "contract", "frontend"
Notes []string `json:"notes,omitempty"`
Skipped bool `json:"skipped,omitempty"`
}
ExplainEntry is one row in the per-file provenance log.
type ExternalSecretEntity ¶
type ExternalSecretEntity struct {
Name string `json:"name"`
Namespace string `json:"namespace"`
Keys []string `json:"keys"`
// Reason is a short human note: why the deploy needs this Secret. Shown
// in the checklist so the operator knows what breaks if it's absent.
Reason string `json:"reason,omitempty"`
// ValueGroup is the shared id tying this Secret to other ExternalSecrets
// that must carry the same logical value (cross-secret byte-match).
// Empty => a standalone prereq (no byte-match group).
ValueGroup string `json:"value_group,omitempty"`
// Source is the rendered forge.StoreSource: nil for a Secret something
// else provisions, set for one ESO syncs from a store.
Source *StoreSourceEntity `json:"source,omitempty"`
}
ExternalSecretEntity mirrors the kcl/schema.k ExternalSecret — an out-of-band Secret a deploy DEPENDS ON but forge does NOT create. forge reads these to print a render-time prerequisite checklist and to drive the deploy preflight (a declared-required-but-absent Secret/key BLOCKS, reusing the same SecretGetter as the secretKeyRef preflight). ValueGroup, when set, ties this Secret to others that must carry the SAME logical value (the cross-secret byte-match group).
type FieldTypeSpec ¶ added in v0.1.45
type FieldTypeSpec struct {
ProtoType string `json:"proto_type"`
SQLType string `json:"sql_type"`
Notes string `json:"notes"`
}
FieldTypeSpec is one row of the proto→column mapping an entity birth applies: the field as declared in the message, the column emitted for it, and the qualification where the column is not simply the scalar type.
type FirebaseRuntime ¶ added in v0.1.45
type FirebaseRuntime struct {
Project string `json:"project"`
Site string `json:"site"`
Target string `json:"target,omitempty"`
Rewrites []map[string]any `json:"rewrites,omitempty"`
// SPAFallback is the entry document served for client-side routes
// ("/index.html"); empty means no SPA fallback.
SPAFallback string `json:"spa_fallback,omitempty"`
// KeepAssetReleases is how many releases' hashed assets the live site
// serves. The render projects it unconditionally (default 10, floor 2).
KeepAssetReleases int `json:"keep_asset_releases"`
}
FirebaseRuntime mirrors forge.OnFirebase.
type FlowCheckEntity ¶ added in v0.1.45
type FlowCheckEntity struct {
Path string `json:"path"`
Description string `json:"description,omitempty"`
Name string `json:"name,omitempty"`
}
FlowCheckEntity mirrors the kcl/workload.k FlowCheck: one app-flow health endpoint a workload serves. Path is the only required field; the URL is resolved at smoke time (see resolveFlowChecks), never declared.
type ForgeDescriptor ¶
type ForgeDescriptor struct {
Services []codegen.ServiceDef `json:"services"`
Configs []codegen.ConfigMessage `json:"configs"`
// Sources fingerprints the .proto files this descriptor came from, one
// entry per file (codegen.DescriptorSources). codegen.loadDescriptor
// REFUSES a descriptor whose entries do not match the protos on disk. It
// is the LAST field: the merge driver settles the block key by key
// (mergeDescriptorSources), so its position must stay fixed.
Sources map[string]string `json:"sources"`
}
ForgeDescriptor is the top-level JSON structure written by mode=descriptor. It aggregates all data the generate.go pipeline needs from proto descriptors. NOTE: this mirrors codegen.ForgeDescriptor — the writer's view of the same file the reader parses. Every field must stay in sync with it, INCLUDING Sources: a field added on one side only would produce a file the other side silently ignores.
type FrontendConfigEntity ¶ added in v0.1.45
type FrontendConfigEntity struct {
APIURL string `json:"api_url,omitempty"`
Mock string `json:"mock,omitempty"`
OTELEndpoint string `json:"otel_endpoint,omitempty"`
Environment string `json:"environment,omitempty"`
TelemetryDisabled bool `json:"telemetry_disabled,omitempty"`
}
FrontendConfigEntity mirrors the kcl/schema.k FrontendConfig — the typed, well-known frontend knobs forge maps to framework-specific env var names (NEXT_PUBLIC_* / VITE_* / EXPO_PUBLIC_*, plus Next.js's NEXT_TELEMETRY_DISABLED) and enforces on the build path. Mock is the load-bearing knob: "off" (default) | "true" | "hybrid". See frontendConfigEnv for the field→variable mapping and the build-path mock force in deploy.go.
type FrontendEntity ¶
type FrontendEntity struct {
ReleaseDependencies map[string]string `json:"release_dependencies,omitempty"`
Name string `json:"name"`
Type string `json:"type,omitempty"` // "nextjs" | "vite-spa" | "react-native"
// Path is the frontend's directory in THIS repository. Empty exactly
// when Source is set — a cross-repo frontend has no directory here
// until the source resolver materializes one.
Path string `json:"path"`
// Source pins the frontend's code to another repository at a ref
// (kcl/schema.k GitSource). nil for the ordinary in-repo frontend.
// Callers that shell into a frontend must resolve this to a directory
// via internal/gitsource rather than reading Path — see
// resolveFrontendEntitySources.
Source *config.GitSource `json:"source,omitempty"`
DevRunner string `json:"dev_runner,omitempty"` // "npm" (default) | "pnpm" | "yarn"
Port int `json:"port,omitempty"`
EnvFile string `json:"env_file,omitempty"`
EnvVars []KCLEnvVar `json:"env_vars,omitempty"`
// Config is the typed, well-known knobs block (kcl/schema.k
// FrontendConfig). nil when the frontend declares no `config`. forge
// expands it into the SAME env stream as EnvVars via frontendConfigEnv,
// with explicit EnvVars winning on a variable collision. See
// EffectiveEnvVars.
Config *FrontendConfigEntity `json:"config,omitempty"`
// Telemetry is the browser telemetry the frontend declares beyond the
// env's observability signals (kcl/schema.k BrowserTelemetry). nil when
// it declares none.
Telemetry *FrontendTelemetryEntity `json:"telemetry,omitempty"`
// Routes is the CRUD-page allowlist (kcl/schema.k Frontend.routes): route
// slugs, or the single value "none". Empty means every CRUD entity.
Routes []string `json:"routes,omitempty"`
// Runtime is where the frontend runs. Always present in a render: the
// Bundle refuses a frontend with none. Zero only for a frontend bridged
// in from forge.yaml (mergeConfigFrontends), which is dev-served.
Runtime FrontendRuntime `json:"runtime"`
// Image is where an OnHosted frontend's site release is pushed: the
// reference the frontend DECLARES, registry host included. Empty for every
// other runtime (the render refuses it there, and requires it on hosted).
// forge appends the platform's static.v1 layout — see
// deploytarget.HostedStaticRepository — so the declared reference stays
// exactly what the author wrote.
Image string `json:"image,omitempty"`
// PublicDir is the build's static output dir relative to the frontend's
// code — declared, else the type's convention (resolved by the render).
PublicDir string `json:"public_dir,omitempty"`
// AssetDir is the build's content-hashed asset directory relative to
// PublicDir — declared, else the type's convention ("assets" for vite,
// "_next/static" for nextjs). Empty when the build has none.
AssetDir string `json:"asset_dir,omitempty"`
// BasePath mounts the frontend under a sub-path ("/admin"); empty is
// the site root.
BasePath string `json:"base_path,omitempty"`
// Bundle is the extra pre-built static dirs assembled into the site.
Bundle []BundleDir `json:"bundle,omitempty"`
// CacheControl is the ordered Cache-Control rules forge applies where it
// publishes the build itself (OnBucket object headers, OnFirebase
// firebase.json headers; the render refuses them elsewhere).
CacheControl []CacheRule `json:"cache_control,omitempty"`
// RuntimeConfig is the frontend's declared `runtime_config` RESOLVED
// to literals by the KCL render (forge.WorkloadURL references lowered
// to the target's URL). It is layered over the typed per-env config
// when forge writes config.js. EMPTY on a hosted env, where the control
// plane resolves references and writes the document itself.
RuntimeConfig map[string]string `json:"runtime_config,omitempty"`
// RuntimeConfigSpec is the same declaration in the StaticSite spec's
// wire shape, references KEPT. The hosted provider publishes it as
// spec.runtimeConfig.
RuntimeConfigSpec map[string]deployv1alpha1.RuntimeConfigValue `json:"runtime_config_spec,omitempty"`
// SourceCommit is the sha Source.Ref resolved to when the source was
// fetched. Empty for an in-repo frontend, and for a pin whose fetch
// could not read its own HEAD.
SourceCommit string `json:"-"`
// SourceOverridden is true when .forge/source-overrides.yaml stood in
// for the pin: the code being built is a local checkout, NOT Source.Ref,
// so the pin must not be reported as what was built.
SourceOverridden bool `json:"-"`
// Release is the release version this build ships under (the deploy's
// bound release), "" when there is none.
Release string `json:"-"`
// Project is the forge project name, handed to the build so the source
// map manifest names it. Not rendered per frontend.
Project string `json:"-"`
// Sourcemaps is "publish" (the default when empty), "public" or "off":
// whether the built source maps leave the served tree for a sidecar, are
// served, or are dropped. See deploytarget.Sourcemaps*.
Sourcemaps string `json:"sourcemaps,omitempty"`
}
FrontendEntity is one frontend from rendered KCL: a dev server, or a static build forge publishes. Runtime says which (ADR 0002 §6), and every consumer dispatches on Runtime.Type. The static build facts (PublicDir, BasePath, Bundle, CacheControl) are the frontend's own, identical on every runtime that publishes a build.
func (FrontendEntity) EffectiveEnvVars ¶ added in v0.1.45
func (f FrontendEntity) EffectiveEnvVars() []KCLEnvVar
EffectiveEnvVars is the frontend's env-var stream forge injects at dev launch and build time: the config-derived variables (frontendConfigEnv) with the explicit EnvVars layered OVER them — an explicit entry for the same variable WINS, and forge warns on the collision (the typed config knob is being shadowed). A frontend with no `config` block returns its EnvVars unchanged, so the legacy env_vars-only shape is byte-identical.
type FrontendRuntime ¶ added in v0.1.45
type FrontendRuntime struct {
Type string
Bucket *BucketRuntime
Firebase *FirebaseRuntime
}
FrontendRuntime is a frontend's resolved runtime. Type carries the tag; Bucket / Firebase are non-nil exactly when Type names them. host, hosted and build-only carry no fields on a frontend.
func (FrontendRuntime) Publishes ¶ added in v0.1.45
func (r FrontendRuntime) Publishes() bool
Publishes reports whether a deploy builds this frontend's static output: every runtime except the dev server.
func (FrontendRuntime) Ships ¶ added in v0.1.45
func (r FrontendRuntime) Ships() bool
Ships reports whether a deploy ships the built site somewhere forge publishes to from this machine (a bucket or Firebase) — the out-of-band frontend providers. Hosted ships through the control plane instead, and build-only ships nowhere.
func (*FrontendRuntime) UnmarshalJSON ¶ added in v0.1.45
func (r *FrontendRuntime) UnmarshalJSON(data []byte) error
UnmarshalJSON dispatches the runtime by its `type` discriminator. An unknown type is an error: the render and this binary disagree about the contract, and a frontend skipped by a stale dispatch table would ship nowhere without anyone noticing.
type FrontendTelemetryEntity ¶ added in v0.1.46
type FrontendTelemetryEntity struct {
ConsoleCapture bool `json:"console_capture"`
ReplaySampleRate float64 `json:"replay_sample_rate"`
Sourcemaps string `json:"sourcemaps,omitempty"`
Errors struct {
Ignore []string `json:"ignore,omitempty"`
} `json:"errors"`
Funnels []FunnelEntity `json:"funnels,omitempty"`
}
FrontendTelemetryEntity mirrors kcl/schema.k BrowserTelemetry.
type FunnelEntity ¶ added in v0.1.46
type FunnelEntity struct {
ID string `json:"id"`
Title string `json:"title,omitempty"`
Steps []string `json:"steps"`
Window string `json:"window"`
By string `json:"by"`
}
FunnelEntity mirrors kcl/schema.k Funnel, window and by already defaulted.
type GRPCRouteEntity ¶
type GRPCRouteEntity struct {
Name string `json:"name"`
Gateway string `json:"gateway"`
Listener string `json:"listener"`
Service string `json:"service,omitempty"`
Workload string `json:"workload,omitempty"`
Port int `json:"port,omitempty"`
Host string `json:"host,omitempty"`
Path string `json:"path,omitempty"`
Traffic *RouteTrafficEntity `json:"traffic,omitempty"`
}
GRPCRouteEntity mirrors the kcl/schema.k GRPCRoute. Shape matches HTTPRouteEntity — the distinction is the rendered Gateway API resource kind (GRPCRoute vs HTTPRoute).
type GatewayAddressEntity ¶
GatewayAddressEntity mirrors the kcl/schema.k GatewayAddress — one entry in the Gateway's spec.addresses, pinning it to a load-balancer address. Type is "NamedAddress" (Value is a GKE reserved static-IP reservation name) or "IPAddress" (Value is a literal IP).
type GatewayEntity ¶
type GatewayEntity struct {
Name string `json:"name"`
GatewayClassName string `json:"gateway_class_name,omitempty"`
Host string `json:"host,omitempty"`
TLS *GatewayTLSEntity `json:"tls,omitempty"`
Listeners []GatewayListenerEntity `json:"listeners,omitempty"`
Addresses []GatewayAddressEntity `json:"addresses,omitempty"`
}
GatewayEntity mirrors the kcl/schema.k Gateway. Listeners are inlined. Tls is nil when the gateway is plaintext.
func (*GatewayEntity) EffectiveHost ¶ added in v0.1.45
func (g *GatewayEntity) EffectiveHost(listener *GatewayListenerEntity, routeHost string) string
EffectiveHost resolves the hostname a route on this listener is served on: the route's own override wins, then the listener's hostname, then the gateway's default host. Mirrors _route_host in kcl/lib/gateway.k — the two must agree, or `forge cluster urls` prints a URL the cluster does not serve.
Returns "" when nothing resolves; callers substitute their own fallback (the dev URL builders use "localhost").
type GatewayListenerEntity ¶
type GatewayListenerEntity struct {
Name string `json:"name"`
Port int `json:"port"`
Protocol string `json:"protocol"`
PathPrefix string `json:"path_prefix,omitempty"`
Hostname string `json:"hostname,omitempty"`
TLS *GatewayTLSEntity `json:"tls,omitempty"`
}
GatewayListenerEntity mirrors the kcl/schema.k GatewayListener. Protocol is "HTTP" | "HTTPS" | "H2C".
Hostname and TLS are the per-listener overrides that let ONE Gateway serve several hostnames, each terminating its own certificate. Both are empty/nil when the listener inherits the parent Gateway's `host` / `tls`; the JSON carries what was DECLARED, not the resolved value, so consumers apply the fallback themselves (see EffectiveHost).
type GatewayTLSEntity ¶
type GatewayTLSEntity struct {
CertIssuer string `json:"cert_issuer,omitempty"`
SecretName string `json:"secret_name,omitempty"`
Certmap string `json:"certmap,omitempty"`
Mode string `json:"mode,omitempty"`
}
GatewayTLSEntity is the TLS block on a Gateway. Mode selects the cert origin: "cert_manager" (default — cert-manager Certificate emitted alongside the Gateway, CertIssuer names a ClusterIssuer), "mkcert" (Secret populated host-side by `forge cluster up` via the mkcert binary; CertIssuer unused), or "gke_certmap" (GCP Certificate Manager map named by Certmap terminates TLS; CertIssuer / SecretName unused — the GKE Gateway controller binds the map via the `networking.gke.io/certmap` annotation forge stamps on the Gateway).
type GenStep ¶
type GenStep struct {
// Name is the human-readable label printed by --plan / --explain
// and used in error wrapping. Replaces the legacy "Step Nx" comment
// numbering — make these stable; tests pin the order.
Name string
// Gate returns true when the step should execute for this context.
// Gates MUST be pure: no I/O, no mutation. They're called by
// stability tests and may be called repeatedly by future watch-mode
// dispatchers.
Gate func(*pipelineContext) bool
// GateReason is the human-readable explanation rendered when Gate
// returns false. Printed by `--plan` `[SKIP]` lines and by the
// `--verbose` `⏩ skipped:` lines instead of the function-name-derived
// `gate: <gateName>` label, which carried no semantic signal for
// users debugging "why didn't generate touch X?".
//
// Conventions:
// - Phrase as the FALSE condition ("no services in forge.yaml",
// "features.codegen=false") so the user reads the skip reason
// directly without having to mentally invert a positive statement.
// - Keep under ~60 chars to fit a single `[SKIP] <name> (<reason>)`
// line at the column width `--plan` already uses (44 + reason).
// - Empty string falls back to the legacy "gate <gateName> returned
// false" rendering — useful during incremental adoption.
GateReason string
// Run is the action. It may mutate ctx (parsed services, derived
// flags, checksums) so subsequent steps can reuse the work. Errors
// abort the pipeline; warnings should be logged in-step.
Run func(*pipelineContext) error
// Tag categorizes the step for future filtering. Conventional
// values: "config", "proto", "codegen", "migrations", "frontend",
// "deploy", "tools", "validate".
Tag string
// Writes declares the project-relative paths (files, or directories,
// captured recursively) an EXTERNAL tool this step runs may write.
// The pipeline captures them into the rollback journal immediately
// before Run, so a failed run restores tool output exactly as it
// restores forge's own writes. Required for every step that shells a
// tool which writes into the project (buf, go mod tidy, …) —
// pinned by TestGenerateStepsDeclareExternalToolOutputs. See
// generate_external_writes.go.
Writes func(*pipelineContext) []string
// ReadOnly declares that the step never writes to the project tree,
// by any route — forge's journaled writers, a raw os.WriteFile, or an
// external tool. It is what lets a failed run say "No files were
// changed" as a computed fact: the claim is made only when every step
// that ran is ReadOnly AND the rollback journal is empty. Default
// false, so a step that is not explicitly vouched for can never
// produce the claim. Pinned by TestGenerateStepsReadOnlySet.
ReadOnly bool
}
GenStep is one ordered unit of the generate pipeline. Steps are pure data plus two functions: Gate decides whether the step should run for the current pipelineContext (must be side-effect free), and Run executes the step. Tag categorizes the step for filtering by future callers (--plan output, watch-mode dispatch).
type GoBuild ¶
type GoBuild struct {
OutputName string `json:"output_name,omitempty"`
Cmd string `json:"cmd"`
GOOS string `json:"goos,omitempty"`
GOARCH string `json:"goarch,omitempty"`
Ldflags []string `json:"ldflags,omitempty"`
Tags []string `json:"tags,omitempty"`
Flags []string `json:"flags,omitempty"`
Env map[string]string `json:"env,omitempty"`
}
GoBuild mirrors the kcl/schema.k GoBuild. Cmd is the go-build target package (e.g. "./cmd/trader"); the rest are the cross-compile + flag knobs build.go passes straight to `go build`.
type HTTPRouteEntity ¶
type HTTPRouteEntity struct {
Name string `json:"name"`
Gateway string `json:"gateway"`
Listener string `json:"listener"`
Service string `json:"service,omitempty"`
Workload string `json:"workload,omitempty"`
Port int `json:"port,omitempty"`
Host string `json:"host,omitempty"`
Path string `json:"path,omitempty"`
// Traffic is the typed per-route policy; nil when the route declares
// none. It replaced a `raw_policy` string that nothing in forge ever
// emitted (ADR-0003 F2).
Traffic *RouteTrafficEntity `json:"traffic,omitempty"`
}
HTTPRouteEntity mirrors the kcl/schema.k HTTPRoute.
The backend is EITHER Service (a Service name, used verbatim) or Workload (a forge.Workload name, resolved per env to that workload's Service or — when it runs OnHost on a local k3d cluster — to the host process via an Envoy Gateway Backend). Exactly one is set.
Port is 0 when it was INFERRED from the workload's single port: this projection carries what was DECLARED, and output.manifests carries the resolved backend. A pre-resolved copy here would be a second source of truth that can disagree with the manifest.
type HandlerPath ¶
type HandlerPath struct {
Service string `json:"service"`
Method string `json:"method"`
Path string `json:"path"`
}
HandlerPath is a single RPC route the binary will register. Service is the fully-qualified proto service name (package.Service); Method is the RPC method name; Path is the canonical Connect URL (`/<Service>/<Method>`).
type HelmChartEntity ¶
type HelmChartEntity struct {
Name string `json:"name"`
// Chart is the chart name for a repo chart; empty for an OCI chart.
Chart string `json:"chart,omitempty"`
// Repo is the chart-repo URL; mutually exclusive with OCI.
Repo string `json:"repo,omitempty"`
// OCI is the OCI chart ref; mutually exclusive with Repo.
OCI string `json:"oci,omitempty"`
// Version is the pinned chart version.
Version string `json:"version"`
// Namespace is the namespace the chart renders into (helm template -n).
Namespace string `json:"namespace"`
// Values is the helm values overlay, passed through verbatim.
Values map[string]any `json:"values,omitempty"`
// CRDs selects which forge-owned CRD bundle to apply FIRST
// (Established-gated) before the chart's controllers: "" (none),
// "gateway-api", "cert-manager" or "external-secrets". The chart is rendered --skip-crds,
// so forge owns the CRD surface.
CRDs string `json:"crds,omitempty"`
// Cluster is the kubectl CONTEXT this chart installs into, overriding
// the env's primary cluster for this chart alone. The KCL `cluster`
// field is a forge.Cluster REFERENCE and the render projects its
// derived `.context` (`k3d-<name>`) here, so the value is always the
// context forge applies with and can never drift from the cluster the
// declaration names. Empty => the env's primary cluster.
//
// This is what lets an operator be installed where its custom
// resources land: control-plane's dev renders `postgresql.cnpg.io/v1
// Cluster` objects into cp-daemon, so CNPG must be installed there and
// not in the env's primary control-plane cluster.
Cluster string `json:"cluster,omitempty"`
// ClusterTarget is true when Cluster came from a forge.ClusterTarget (a
// cluster you operate) rather than a forge.Cluster. A target is its own
// declaration, so validateChartCluster does not look for it in the env's
// k3d clusters.
ClusterTarget bool `json:"cluster_target,omitempty"`
// Manifests are consumer-declared raw k8s manifest dicts that ride this
// chart's `--target` (the `eg` GatewayClass, cert-manager ClusterIssuers)
// — the cluster-scoped instances the chart's controller reconciles but
// the chart itself doesn't ship. Stamped with the chart's app-label and
// applied AFTER its controllers; excluded from a bare app deploy.
Manifests []any `json:"manifests,omitempty"`
// NamespaceLabels / NamespaceAnnotations are written onto the Namespace
// forge creates for the chart (e.g. Pod Security Admission labels).
NamespaceLabels map[string]string `json:"namespace_labels,omitempty"`
NamespaceAnnotations map[string]string `json:"namespace_annotations,omitempty"`
// Prune says whether a re-apply deletes the chart's objects that left the
// render. A pointer so a contract that omits it (an older render) reads as
// the schema default, true, rather than as false.
Prune *bool `json:"prune,omitempty"`
// Patches are merge patches for the objects the chart renders, keyed
// `Kind/name` (see cluster.HelmChartSpec.Patches).
Patches map[string]any `json:"patches,omitempty"`
}
HelmChartEntity is one declared platform dependency from rendered KCL (the `output.helm_charts` projection of forge.HelmChart). It is the declaration only — forge expands it via `helm template --skip-crds` Go-side (internal/cluster.RenderHelmChart) and folds the manifests into the apply stream, selected by `--target=<name>`. helm is a RENDERER, not an installer: there is no release, no `helm install`.
func (HelmChartEntity) PruneEnabled ¶ added in v0.1.45
func (c HelmChartEntity) PruneEnabled() bool
PruneEnabled reports the chart's prune setting: on unless declared off.
type HostInfraEntity ¶ added in v0.1.45
type HostInfraEntity struct {
Name string `json:"name"`
Engine string `json:"engine,omitempty"`
Port int `json:"port,omitempty"`
Database string `json:"database,omitempty"`
User string `json:"user,omitempty"`
Password string `json:"password,omitempty"`
DataDir string `json:"data_dir,omitempty"`
Version string `json:"version,omitempty"`
// engine = "zitadel" only — the dev IdP's backing database and its
// declarative bootstrap. See the HostInfra schema in kcl/schema.k.
IDPDatabase string `json:"idp_database,omitempty"`
IDPDatabasePort int `json:"idp_database_port,omitempty"`
IDPMasterKey string `json:"idp_masterkey,omitempty"`
IDPStepsFile string `json:"idp_steps_file,omitempty"`
IDPPATPath string `json:"idp_pat_path,omitempty"`
// engine = "nats" only.
NATSMonitorPort int `json:"nats_monitor_port,omitempty"`
// engine = "temporal" only.
TemporalNamespaces []string `json:"temporal_namespaces,omitempty"`
TemporalUIPort int `json:"temporal_ui_port,omitempty"`
}
HostInfraEntity is one `Bundle.infra` entry: a third-party server forge runs as a HOST PROCESS instead of a container — the default shape for dev infrastructure on a machine that cannot afford a container runtime. See internal/hostinfra for the supervisor and internal/deploytarget.HostInfraProvider for the dispatch.
type HostRuntime ¶ added in v0.1.45
type HostRuntime struct {
Runner string `json:"runner,omitempty"` // "go-run" | "air" | "binary" | "delve"
AirConfig string `json:"air_config,omitempty"` // default .air.toml
// WorkingDir overrides the launched subprocess's working directory.
// Relative paths resolve against the project root. Use this for
// cross-repo binaries whose runner config (e.g. Air's build_cmd
// paths) resolves relative to a sibling repo. Default: project root.
WorkingDir string `json:"working_dir,omitempty"`
// ListenPorts are the host TCP ports this workload itself BINDS. When
// declared, the `forge env up` port-conflict pre-flight and readiness
// gate check EXACTLY these; otherwise the workload's declared
// spec.ports stand in.
//
// A POINTER so "not declared" and "declares zero ports" stay distinct.
// A host workload that legitimately binds nothing (a packaged desktop
// app) declares `listen_ports = []`; forge must then allocate nothing
// and gate on nothing.
ListenPorts *[]int `json:"listen_ports,omitempty"`
DelvePort int `json:"delve_port,omitempty"` // runner delve; default 2345
// LaunchEnv is RUN state, not contract: env forge decides for this one
// launch (the ephemeral PORT resolveEphemeralHostPorts allocates) and
// layers over the workload's declared spec.env. It is never part of
// the render — a per-run value must not look like a declaration.
LaunchEnv map[string]string `json:"-"`
}
HostRuntime is forge.Host: run the workload as a local process. The argv is NOT declared here — hostlaunch.BuildCmd derives it from the runner, the workload's build and its args. Env comes from the workload's spec.env.
type HostedRuntime ¶ added in v0.1.45
type HostedRuntime struct {
Platform string `json:"platform,omitempty"` // GOARCH of the platform's nodes
}
HostedRuntime is forge.OnHosted. The control plane owns every placement fact except the node architecture, which forge needs to build the image.
type IngressURL ¶
type IngressURL struct {
Route string `json:"route"`
Kind string `json:"kind"` // "HTTPRoute" | "GRPCRoute"
URL string `json:"url"`
Gateway string `json:"gateway"`
Listener string `json:"listener"`
Service string `json:"service"`
Port int `json:"port"`
Warning string `json:"warning,omitempty"`
}
IngressURL is one row of the dev ingress URL table.
Warning is set (and URL may be empty) when the route references a gateway/listener that doesn't resolve. Surfacing this as a row rather than aborting matches the audit-shaped commands' graceful-degradation posture.
type KCLEntities ¶
type KCLEntities struct {
// OptionalSkipped names the optional compose workloads (and the workloads
// requiring them) `forge env up` dropped from this run: because the Docker
// daemon is unreachable, or because no --target selected or requires them
// (scopeOptionalInfra). Run state, never part of the render.
OptionalSkipped []string `json:"-"`
ReleaseManifestConsumers []releaseManifestConsumer
// Project and Env are the bundle's project name and the env it was
// rendered for. ImageTag is the env's RESOLVED image tag — the tag the
// Cluster workload records' images carry, and therefore the tag
// `forge build <env>` defaults to so build and deploy agree by
// construction.
Project string `json:"project,omitempty"`
Env string `json:"env,omitempty"`
ImageTag string `json:"image_tag,omitempty"`
// Lifecycle is the env's DECLARED apply path (kcl/schema.k
// Bundle.lifecycle): "local" (a developer's own cluster),
// "ephemeral" (a throwaway per-run cluster), or EMPTY for a real
// environment — one reconciled from a bundle rather than applied to
// directly.
//
// Empty is the meaningful default, not a gap: an env that declares
// nothing is treated as a real one. Read it through
// DirectApplyAllowed rather than comparing strings, so the
// no-cluster case stays in one place.
Lifecycle string `json:"lifecycle,omitempty"`
// Applier is "forge" when the env DECLARES that forge applies it
// (Bundle.applier): a real environment, not a scratch one, none of whose
// delivery units has moved onto the hub yet. Transitional. Empty
// otherwise. Read it through unitSplitOf and DirectApplyAllowed rather
// than comparing strings.
Applier string `json:"applier,omitempty"`
// DeliveryUnits are the units the env's bundle is split into, in
// declaration order (Bundle.delivery_units). Empty: everything is in the
// default unit.
DeliveryUnits []release.DeliveryUnit
// HubConverges is the Bundle.hub_converges declaration: the delivery units
// the hub converges, with every other unit applied by forge directly. NIL
// means the env declared none (forge keeps its inference); a non-nil empty
// slice is a declaration that the hub converges nothing. Read it through
// unitSplitOf rather than directly.
HubConverges []string
// Clusters are the k3d clusters forge ensures exist at the head of
// `forge env up` before any workload deploys. Empty for an env that
// declares no clusters. Ownership is implicit via Cluster.Network /
// Cluster.RegistryMirror — there is no "primary" cluster.
Clusters []ClusterEntity `json:"clusters,omitempty"`
// ClusterTarget is the Bundle's DECLARED env-wide target — the kubectl
// context, namespace, registry and platform the env's support
// resources (Namespace, ConfigMaps, gateways) deploy to, stated once.
// Nil for an env that declares none (host-only, compose-only, hosted).
//
// Every env-wide question (which namespace, which context, which arch)
// is answered from HERE when it is present, never inferred from "the
// first cluster workload".
ClusterTarget *ClusterTargetEntity `json:"cluster_target,omitempty"`
// KubeconfigSecrets are cross-cluster kubeconfigs forge mints fresh
// each up (at the cluster→deploy boundary) and applies as k8s Secrets.
KubeconfigSecrets []KubeconfigSecretEntity `json:"kubeconfig_secrets,omitempty"`
// ConnectedClusters binds a kubectl context to its connected cluster once,
// at cluster level; every group placed on that context inherits it.
ConnectedClusters map[string]string `json:"connected_clusters,omitempty"`
// Workloads is every runnable thing in the env, of every kind and
// every runtime, in declaration order. See WorkloadEntity.
Workloads []WorkloadEntity `json:"-"`
// Infra are the env's host-run third-party servers (forge.HostInfra:
// postgres, zitadel). They are dialled by workloads, not declared by
// the app, so they are not Workloads (P2 map §3.5).
Infra []HostInfraEntity `json:"infra,omitempty"`
Frontends []FrontendEntity `json:"frontends,omitempty"`
Gateways []GatewayEntity `json:"gateways,omitempty"`
HTTPRoutes []HTTPRouteEntity `json:"http_routes,omitempty"`
GRPCRoutes []GRPCRouteEntity `json:"grpc_routes,omitempty"`
// HelmCharts are the env's declared platform deps (forge.HelmChart),
// each a renderable with a NAME the `--target` axis selects. forge
// expands them via helm-as-a-RENDERER and folds the manifests into the
// apply stream. Empty => no platform deps. See HelmChartEntity.
HelmCharts []HelmChartEntity `json:"helm_charts,omitempty"`
// Databases are the env's managed databases (forge.ManagedDatabase),
// each bound to the cluster or hosted runtime. See DatabaseEntity.
Databases []DatabaseEntity `json:"databases,omitempty"`
// SecretProvider is the bundle-level secret provider declaration
// (WHERE secret values come from for this env). Nil when the bundle
// declares no provider.
SecretProvider *SecretProviderEntity `json:"secret_provider,omitempty"`
// Observability is the env's typed observability declaration. Nil =
// telemetry off. See ObservabilityEntity.
Observability *ObservabilityEntity `json:"observability,omitempty"`
// DeliveryPolicy is Bundle.delivery_policy, every control resolved. Nil
// when the env declares none (every control at its default).
DeliveryPolicy *release.ShapeDeliveryPolicy `json:"delivery_policy,omitempty"`
// DataAccess is Bundle.data_access, resolved: the declared db_query
// policy, else the default the env's lifecycle implies (open for a local
// or ephemeral env, audited otherwise). Nil when that is audited, the
// default: absence means audited, never open.
DataAccess *release.ShapeDataAccess `json:"data_access,omitempty"`
// ReleaseHealth is Bundle.release_health, resolved. Nil when the env
// declares none (the defaults).
ReleaseHealth *release.ShapeReleaseHealth `json:"release_health,omitempty"`
// UptimeChecks are Bundle.uptime resolved: the declared checks plus the
// ones forge derives for a hosted env. Empty when the env has none.
UptimeChecks []release.ShapeUptimeCheck `json:"uptime_checks,omitempty"`
// Identity is Bundle.identity, resolved by the render: the applications
// and consumers derived, the deliveries and reserved managed-secret names
// listed. Nil when the env declares none. See release.ShapeIdentity: the
// render emits the shape's own vocabulary, so there is nothing to
// translate between the two.
Identity *release.ShapeIdentity `json:"identity,omitempty"`
// RenderedSecrets are the Bundle-level forge.RenderedSecret
// declarations (Bundle.rendered_secrets): Secrets forge renders and
// applies itself, INDEPENDENT of SecretProvider, each at the
// cluster/namespace it declares (KCL resolves the cluster_target
// default, so Cluster is always set). Empty => none.
RenderedSecrets []RenderedSecretEntity `json:"rendered_secrets,omitempty"`
// ControlPlane is the bundle-level control-plane declaration (WHICH
// endpoint this env talks to, and the env var NAME its credential is
// read from). It is NOT an env mode: it is required when some workload
// binds the Hosted runtime, and otherwise serves as the env's secret
// store. Nil when the bundle declares none.
ControlPlane *ControlPlaneEntity `json:"control_plane,omitempty"`
// RequiredSecrets are the env's declared external Secret prerequisites
// (forge.ExternalSecret) — out-of-band Secrets the deploy depends on but
// forge does NOT create. Drive the render-time checklist + the deploy
// preflight BLOCK on a declared-required-but-absent Secret/key.
RequiredSecrets []ExternalSecretEntity `json:"required_secrets,omitempty"`
// OpenBao and BootstrapSource are the rendered Bundle.openbao and
// Bundle.bootstrap_source: where `forge secret` reaches a self-run store
// and where BootstrapSecrets live. Nil when the env declares none.
OpenBao *OpenBaoEntity `json:"openbao,omitempty"`
BootstrapSource *BootstrapSourceEntity `json:"bootstrap_source,omitempty"`
// ExternalSecretsOperators are the ESO installs (forge.ExternalSecretsOperator)
// the release shape records.
ExternalSecretsOperators []release.RenderedExternalSecretsOperator `json:"external_secrets_operators,omitempty"`
// RequiredDNS are the env's declared DNS-record prerequisites
// (forge.DNSRecord) — surfaced as a render-time checklist note (forge
// can't authoritatively verify external DNS). Empty => none.
RequiredDNS []DNSRecordEntity `json:"required_dns,omitempty"`
// ControlPlaneSecrets are the Secrets the env's control plane supplies and
// renews (forge.ControlPlaneSecret), including the ones forge derives for a
// control-plane Observability destination. Recorded in the env shape; never
// a deploy precondition.
ControlPlaneSecrets []ControlPlaneSecretEntity `json:"control_plane_secrets,omitempty"`
// ManifestNamespace is the namespace that dominates the rendered
// `output.manifests` stream. It is the fallback for the declared
// namespace when the bundle declares no cluster_target and no cluster
// workload. Empty when the stream carries no namespaced object.
ManifestNamespace string `json:"-"`
// ManifestServiceNames are the metadata.name of every raw k8s Service
// in `output.manifests` — Services a project injects via KCL (e.g.
// `additional_manifests`, a workload's owned manifests) that are not a
// workload of their own. The ingress audit unions these into the
// known-backend set so a route targeting such a Service resolves
// instead of false-erroring "unknown service".
ManifestServiceNames []string `json:"-"`
// ManifestClusters are the kubectl contexts the rendered stream stamps
// objects onto (forge.dev/cluster). Each is a deploy destination:
// buildDeployGroups gives one that no workload runs on a k8s group of
// its own, so a forge.Manifests group on an otherwise empty cluster is
// applied there instead of silently never applied.
ManifestClusters []ManifestClusterEntity `json:"-"`
// ManifestUnits are the delivery units raw forge.Manifests objects are
// stamped with (forge.dev/delivery-unit), sorted and de-duplicated. Together
// with the workloads' units and Bundle.delivery_units they are the universe
// a Bundle.hub_converges split is drawn from.
ManifestUnits []string `json:"-"`
// ClusterWork names ("Kind/name") every rendered object that DOES
// something on a cluster — everything in `output.manifests` except the
// env's own support objects: its Namespaces, and the Gateways it
// declares in Bundle.gateways. A Namespace holds nothing by itself and
// a Gateway routes nothing until a route attaches to it, so an env
// whose stream is ONLY those has placed no work in a cluster, however
// many cluster_target / gateway declarations it carries. Read through
// envClusterDemand, which also counts the cluster work forge applies
// outside the stream (helm charts, placed Secrets, minted kubeconfigs).
ClusterWork []string `json:"-"`
}
func RenderKCL ¶
func RenderKCL(ctx context.Context, projectDir, env string) (*KCLEntities, error)
RenderKCL shells `kcl run deploy/kcl/<env>/ -o json`, parses the output, and dispatches each service's deploy block by Type into the right pointer. Returns an error when:
- The KCL directory doesn't exist (env not configured)
- `kcl` is not on PATH (caller needs to install it)
- The JSON output is malformed
- A service's deploy.type is none of "host"/"cluster"/"build-only"
The override env var FORGE_KCL_RENDER_FIXTURE points at a JSON file whose contents are read in lieu of shelling kcl. Used by unit tests so they can exercise the dispatch logic without a real KCL toolchain.
func RenderKCLWith ¶ added in v0.1.45
func RenderKCLWith(ctx context.Context, projectDir, env string, extra []string) (*KCLEntities, error)
RenderKCLWith is RenderKCL plus additional forge-derived `-D` bindings.
It exists for the build render, which must bind values a plain render cannot know: `image_tag` (the tag THIS build resolved, which outranks the env's own default) and `target_arch`. Both are read by the KCL that composes a ShellBuild's `cmd`, and that command is run verbatim — so if forge did not bind them before the render, the command string would carry the wrong tag or arch and there is no later substitution pass to correct it.
extra is `key=value` with the value already KCL-quoted by the caller, the same contract renderKCLRaw's own dArgs use. A key the caller binds here wins over nothing else: these are reserved names no project may set.
func (*KCLEntities) FindWorkload ¶ added in v0.1.45
func (e *KCLEntities) FindWorkload(name string) *WorkloadEntity
FindWorkload returns the named workload, or nil. name may be the workload's declared name or its component's (`admin_server` finds the workload declared `admin-server`; see codegen.WorkloadName).
func (*KCLEntities) HasHosted ¶ added in v0.1.45
func (e *KCLEntities) HasHosted() bool
HasHosted reports whether anything in the env is bound to the control plane: a Hosted workload, a hosted database, or an OnHosted frontend.
func (*KCLEntities) HasLocal ¶ added in v1.0.0
func (e *KCLEntities) HasLocal() bool
HasLocal reports whether anything of the env runs on this machine — a host process, a docker compose service, or a frontend dev server — and so can feed the local ClickStack `forge env up` starts. An env whose workloads all deploy to a cluster (or to hosting) has nothing local to query.
func (*KCLEntities) WorkloadNames ¶ added in v0.1.45
func (e *KCLEntities) WorkloadNames(runtime string) []string
WorkloadNames returns the names of every workload bound to the named runtime (RuntimeHost, RuntimeCluster, ...), in declaration order. An empty runtime returns every workload's name.
func (*KCLEntities) WorkloadsOn ¶ added in v0.1.45
func (e *KCLEntities) WorkloadsOn(runtime string) []WorkloadEntity
WorkloadsOn returns the workloads bound to the named runtime, in declaration order.
type KCLEnvVar ¶
type KCLEnvVar struct {
Name string `json:"name"`
Value string `json:"value,omitempty"`
SecretRef string `json:"secret_ref,omitempty"`
SecretKey string `json:"secret_key,omitempty"`
ConfigMapRef string `json:"config_map_ref,omitempty"`
ConfigMapKey string `json:"config_map_key,omitempty"`
// SecretOptional exempts this var's secret_ref from the store
// pre-flight. Set by config codegen from `optional: true` on a
// `sensitive` proto field — never hand-authored.
SecretOptional bool `json:"secret_optional,omitempty"`
}
KCLEnvVar is a single env var entry from the rendered KCL. Distinct type so we don't pull in the project-config EnvVar (which carries codegen-specific fields the KCL renderer doesn't know about).
Three projection channels mirror the KCL EnvVar schema (kcl/schema.k):
- Value: inline literal. The dominant case host-mode consumes.
- SecretRef + SecretKey: cluster-mode projection from a Secret (Deployment.env.valueFrom.secretKeyRef). No host equivalent — host-mode picks the value up from the gitignored secrets_file.
- ConfigMapRef + ConfigMapKey: cluster-mode projection from a forge-generated ConfigMap.
SecretRef / ConfigMapRef are surfaced (rather than dropped) so the `forge doctor parity` diff can attribute cluster-side projected env vars to their source rather than treating an empty Value as "unset".
type KubeconfigPolicyRuleEntity ¶ added in v0.1.45
type KubeconfigPolicyRuleEntity struct {
APIGroups []string `json:"api_groups,omitempty"`
Resources []string `json:"resources"`
Verbs []string `json:"verbs"`
// ResourceNames limits the rule to these named objects. Empty means every
// object of Resources.
ResourceNames []string `json:"resource_names,omitempty"`
}
KubeconfigPolicyRuleEntity mirrors kcl/schema.k's KubeconfigPolicyRule — one rbac.authorization.k8s.io/v1 PolicyRule.
type KubeconfigSecretEntity ¶
type KubeconfigSecretEntity struct {
Name string `json:"name"`
InCluster string `json:"in_cluster"`
TargetCluster string `json:"target_cluster"`
ContextName string `json:"context_name"`
Key string `json:"key,omitempty"`
Namespace string `json:"namespace,omitempty"`
Reachability string `json:"reachability,omitempty"`
// TargetContext is the kubectl context addressing the target
// cluster. Empty means the k3d derivation (`k3d-<TargetCluster>`,
// resolved via `k3d kubeconfig get`). Set, it names a context in the
// operator's own kubeconfig — which is how a non-k3d cluster (GKE)
// becomes addressable at all.
TargetContext string `json:"target_context,omitempty"`
// ServiceAccount, when non-nil, switches the mint from COPYING the
// operator's credential to MINTING one on the target. See
// KubeconfigServiceAccountEntity.
ServiceAccount *KubeconfigServiceAccountEntity `json:"service_account,omitempty"`
}
KubeconfigSecretEntity mirrors the kcl/schema.k KubeconfigSecret — a cross-cluster kubeconfig forge mints FRESH each up and stores as a k8s Secret. The mint step (mintKubeconfigSecrets) resolves the target's endpoint at runtime and never persists the IP.
type KubeconfigServiceAccountEntity ¶ added in v0.1.45
type KubeconfigServiceAccountEntity struct {
Name string `json:"name"`
Namespace string `json:"namespace"`
// Namespaces, when non-empty, binds Rules as a Role in each listed
// namespace instead of one cluster-wide ClusterRole.
Namespaces []string `json:"namespaces,omitempty"`
Rules []KubeconfigPolicyRuleEntity `json:"rules"`
}
KubeconfigServiceAccountEntity mirrors kcl/schema.k's KubeconfigServiceAccount — the ServiceAccount + RBAC + long-lived token forge converges ON THE TARGET cluster so the minted kubeconfig carries a credential a POD can present.
This is the whole of defect F1b: a GKE kubeconfig authenticates with an `exec` plugin that resolves only on the operator's machine, so copying it into a Secret produces a kubeconfig no pod can use. Minting sidesteps the operator's credential entirely.
type LibrariesSpec ¶ added in v0.1.45
type LibrariesSpec struct {
Module string `json:"module"`
Version string `json:"version,omitempty"`
Dir string `json:"dir"`
Packages []LibrarySpec `json:"packages,omitempty"`
// WebRuntime is nil when no frontend in this project depends on it.
WebRuntime *WebRuntimeSpec `json:"web_runtime,omitempty"`
// SignatureSelectors echoes what --signatures asked for, so a pasted
// brief records which slice of the API it carries — and which it does
// not. Empty when signatures were not requested.
SignatureSelectors string `json:"signature_selectors,omitempty"`
// Divergence is non-nil when the module resolution above (the one
// the build actually uses) does not match what an OFFLINE `go doc`
// consults — a go.work `use`/replace pointing forge/pkg at a local
// checkout. nil means the two agree, which is the overwhelmingly
// common case and prints nothing extra.
Divergence *LibraryDivergence `json:"divergence,omitempty"`
}
LibrariesSpec is the whole dump: the resolved Go module and its packages, then the frontend runtime. forge ships two runtime libraries — forge/pkg for Go and @reliantlabs/forge-web-runtime for the web — and a verb that indexed only the first sent every agent looking for the second onto the filesystem.
type LibraryDivergence ¶ added in v0.1.45
type LibraryDivergence struct {
// GoModVersion is the version github.com/reliant-labs/forge/pkg is
// pinned to in this project's go.mod requirement.
GoModVersion string `json:"go_mod_version"`
// ResolvedDir echoes LibrariesSpec.Dir — the directory the build
// actually uses. Repeated here so a consumer of just the
// divergence field doesn't have to cross-reference the parent.
ResolvedDir string `json:"resolved_dir"`
// Source names what caused the override: "go.work" when GOWORK
// pointed at an active workspace file, "replace" when a go.mod
// replace directive did it with no workspace involved.
Source string `json:"source"`
}
LibraryDivergence records a resolved-vs-module-cache mismatch for forge/pkg: the version pinned in go.mod (what a `go doc` run against the module cache alone would show) versus the directory this project actually builds against (a go.work `use` or a go.mod `replace` pointing at a local checkout).
It exists because `go doc`'s answer depends on ambient state (GOWORK, which directory the invoker's shell happens to be in, whether a subprocess inherited it) in a way the build itself does not: the compiler always resolves through the active workspace/replace, so an API read that skips it is describing a package that is not the one linked into the binary.
type LibrarySpec ¶ added in v0.1.45
type LibrarySpec struct {
Name string `json:"name"` // e.g. "svcerr"
ImportPath string `json:"import_path"` // e.g. "github.com/reliant-labs/forge/pkg/svcerr"
Dir string `json:"dir"` // absolute path to the package source
Synopsis string `json:"synopsis,omitempty"` // first sentence of the package doc
// Symbols is the package's exported surface, present only for the
// packages --signatures selected. See libraries_signatures.go.
Symbols []LibrarySymbol `json:"symbols,omitempty"`
}
LibrarySpec is one forge/pkg subpackage: what to import, where its source is, and the opening line of its own package doc.
type LibrarySymbol ¶ added in v0.1.45
type LibrarySymbol struct {
// Kind is "const", "var", "func", "method" or "type".
Kind string `json:"kind"`
// Name is the exported identifier; for a method, "Type.Method".
Name string `json:"name"`
// Decl is the declaration as printed from source.
Decl string `json:"decl"`
}
LibrarySymbol is one exported declaration of a forge/pkg package, as that package's own source declares it.
Decl is the rendered declaration: the full signature for a func, the exported fields for a struct, the method set for an interface. It is produced by go/printer from the parsed source, so it cannot say something the source does not.
type LiveDrift ¶ added in v0.1.45
type LiveDrift struct {
// State is in_sync | drifted | unknown. UNKNOWN IS NOT in_sync.
State string
BundleID string
ApplyID string
ObservedAt *time.Time
// Objects holds only the objects with something to say; an in-sync env
// reports an empty list rather than every object it owns.
Objects []LiveDriftObject
Detail string
}
LiveDrift is what an observer saw of the live cluster, against the applied bundle (F-17). A REPORT: it never refuses a deploy, never rolls anything back and never feeds policy. It does appear in the deploy plan, so an operator reviewing a plan sees that the deploy will overwrite an out-of-band change.
type LiveDriftObject ¶ added in v0.1.45
type LiveDriftObject struct {
Key release.ObjectKey
ExpectedHash string
ObservedHash string
// State is in_sync | drifted | missing | unobservable.
State string
}
LiveDriftObject is one object's drift verdict.
type LiveEnvironment ¶ added in v0.1.45
type LiveEnvironment struct {
// Env is the environment's record: name, kind, and the declaration.
Env release.EnvRecord
// EnvironmentID is the control plane's id, for the per-env RPCs a
// reader may follow up with.
EnvironmentID string
Namespace string
// ConvergesPromotions: a promotion here is applied server-side. False
// means nothing moves without a client-side deploy, so a caller should
// refuse fast rather than wait out a timeout.
ConvergesPromotions bool
// CurrentPromotion and CurrentRelease are where the IMAGES come from.
CurrentPromotion *release.Promotion
CurrentRelease *release.Release
// CurrentBundle is where the CONFIG comes from.
CurrentBundle *release.BundleRecord
// LatestApply may be newer than CurrentBundle and still running.
LatestApply *ApplyRecord
// Phase is the rollout phase, as the wire enum names it. Rendered with
// rolloutPhaseName; UNSPECIFIED for a self-managed env until applies
// exist.
Phase string
// Sessions are the live `forge env up` stacks on a LOCAL env: one row
// per worktree per machine.
Sessions []release.LocalSession
// Drift is nil when drift is NOT OBSERVABLE for this env — never
// confused with "no drift found", which is a Drift whose Objects are
// empty and whose State is in_sync.
Drift *LiveDrift
}
LiveEnvironment is one env as Live shows it, in forge's own vocabulary.
Every pointer is nilable and every nil is MEANINGFUL: no promotion means never deployed, no bundle means no config has been applied, no drift means drift is not observable here. A reader must render those three differently from their empty equivalents, which is why none of them is flattened into a zero value.
type ManifestClusterEntity ¶ added in v0.1.45
ManifestClusterEntity is one kubectl context the rendered stream stamps an object onto (`forge.dev/cluster`), with the namespace its namespaced objects dominate there ("" when every object on it is cluster-scoped).
type MapNode ¶
type MapNode struct {
Path string `json:"path"`
Name string `json:"name"`
IsDir bool `json:"is_dir"`
Ownership string `json:"ownership,omitempty"`
Flags []string `json:"flags,omitempty"`
Children []*MapNode `json:"children,omitempty"`
}
MapNode is one entry in the project tree. Children is empty for files; for directories it is sorted alphabetically (dirs first, then files, each group ordered by name).
type MarkerSpec ¶ added in v0.1.45
type MarkerSpec struct {
Name string `json:"name"`
AppliesTo string `json:"applies_to"`
Effect string `json:"effect"`
Placement string `json:"placement"`
Example string `json:"example"`
}
MarkerSpec describes one `// forge:*` comment marker. AppliesTo is the descriptor level it attaches to ("entity" for the message-level markers, "field" for the field-level ones).
type ObservabilityDestEntity ¶ added in v0.1.45
type ObservabilityDestEntity struct {
Endpoint string `json:"endpoint"`
Protocol string `json:"protocol"`
HeadersSecret *struct {
Name string `json:"name"`
Key string `json:"key"`
} `json:"headers_secret,omitempty"`
// TokenSecret is a bearer token the node agent re-reads from a mounted file.
TokenSecret *struct {
Name string `json:"name"`
Key string `json:"key"`
} `json:"token_secret,omitempty"`
// TokenFromControlPlane is true when the token is the one the control
// plane delivers (a ControlPlaneSecret forge declared); TokenSecret then
// names where it lands.
TokenFromControlPlane bool `json:"token_from_control_plane,omitempty"`
}
ObservabilityDestEntity is Observability.destination.
type ObservabilityEntity ¶ added in v0.1.45
type ObservabilityEntity struct {
Enabled bool `json:"enabled"`
LocalPort int `json:"local_port,omitempty"`
LocalEndpoint string `json:"local_endpoint,omitempty"`
// ProfilesEndpoint is the full URL of the local profiles collector host
// processes push to; "" when profiles are off or go to a destination.
ProfilesEndpoint string `json:"profiles_endpoint,omitempty"`
// AlertmanagerPort and AlertsPort are the local alerting loop's loopback
// ports: the compose Alertmanager's and the evaluator's (its in-app
// sink). Zero when the env runs no local loop (no ports declared, or a
// destination instead of the local ClickStack).
AlertmanagerPort int `json:"alertmanager_port,omitempty"`
AlertsPort int `json:"alerts_port,omitempty"`
Destination *ObservabilityDestEntity `json:"destination,omitempty"`
Signals ObservabilitySignalsEntity `json:"signals"`
// Policy is the resolved opt-out, recorded in the release shape.
Policy *ObservabilityPolicyEntity `json:"policy,omitempty"`
Sampling struct {
TracesRatio float64 `json:"traces_ratio"`
ErrorsAlways bool `json:"errors_always"`
DestinationTailSamples bool `json:"destination_tail_samples"`
} `json:"sampling"`
Retention struct {
TracesDays int `json:"traces_days"`
MetricsDays int `json:"metrics_days"`
LogsDays int `json:"logs_days"`
// The browser-telemetry tables' TTLs, in days (always rendered,
// defaulted by the schema).
ReplayDays int `json:"replay_days"`
ErrorsDays int `json:"errors_days"`
ProductEventsDays int `json:"product_events_days"`
} `json:"retention"`
Agent struct {
Namespace string `json:"namespace"`
Image string `json:"image"`
MemoryLimitMiB int `json:"memory_limit_mib"`
} `json:"agent"`
// Alerts is Observability.alerts as the author declared it; nil when none.
Alerts *AlertsEntity `json:"alerts,omitempty"`
}
ObservabilityEntity is the env's typed observability declaration (Bundle.observability) as `output.observability`. Nil when the env declares none: telemetry is off and nothing is injected.
type ObservabilityPolicyEntity ¶ added in v0.1.45
type ObservabilityPolicyEntity struct {
Enabled bool `json:"enabled"`
Disabled []string `json:"disabled,omitempty"`
// OptIn names the default-off categories the env turned on (replay).
OptIn []string `json:"opt_in,omitempty"`
}
ObservabilityPolicyEntity is Observability's resolved opt-out: Disabled names the categories (traces, metrics, logs, profiles, metadata, browser) the author switched off, every one when Enabled is false.
type ObservabilitySignalsEntity ¶ added in v0.1.45
type ObservabilitySignalsEntity struct {
Traces bool `json:"traces"`
Metrics bool `json:"metrics"`
Logs bool `json:"logs"`
Profiles bool `json:"profiles"`
Metadata bool `json:"metadata"`
Browser bool `json:"browser"`
Replay bool `json:"replay"`
}
ObservabilitySignalsEntity is Observability.signals.
type OpenBaoBootstrapEntity ¶ added in v1.0.0
type OpenBaoBootstrapEntity struct {
Project string `json:"project"`
// Namespace is the store's, where both service accounts live.
Namespace string `json:"namespace"`
ReaderServiceAccount string `json:"reader_service_account"`
ReaderGSA string `json:"reader_gsa"`
EscrowServiceAccount string `json:"escrow_service_account"`
EscrowGSA string `json:"escrow_gsa"`
ESONamespace string `json:"eso_namespace"`
SealSecret string `json:"seal_secret"`
PasswordSecret string `json:"password_secret,omitempty"`
EscrowSecret string `json:"escrow_secret"`
}
OpenBaoBootstrapEntity is the resolved Secret Manager delivery: the Google identities it runs as, and which secrets it moves. Declarations only.
type OpenBaoEntity ¶ added in v1.0.0
type OpenBaoEntity struct {
Name string `json:"name"`
Namespace string `json:"namespace"`
Image string `json:"image,omitempty"`
ReleaseOwner string `json:"release_owner,omitempty"`
ReleaseInputs map[string]string `json:"release_inputs,omitempty"`
// Cluster is the kube context the store runs in; ConnectedCluster is the
// name `forge cluster connect` registered.
Cluster string `json:"cluster,omitempty"`
ConnectedCluster string `json:"connected_cluster,omitempty"`
// ServerURL is the in-cluster address (https://<name>.<ns>.svc:<port>).
ServerURL string `json:"server_url,omitempty"`
CAConfigMap string `json:"ca_configmap,omitempty"`
KVMount string `json:"kv_mount,omitempty"`
// SelfCluster is the store's own cluster id: its auth mount is jwt-<id>.
SelfCluster string `json:"self_cluster,omitempty"`
AuthMount string `json:"auth_mount,omitempty"`
// Audience is what the cluster's tokens must carry to log in.
Audience string `json:"audience,omitempty"`
ServiceAccount string `json:"service_account,omitempty"`
Org string `json:"org,omitempty"`
EnvDir string `json:"env,omitempty"`
// BootstrapNames are the BootstrapSecret names the component declares that
// a person may import (the seal key, the Postgres password and the recovery
// escrow).
BootstrapNames []string `json:"bootstrap_names,omitempty"`
// BootstrapGenerate are the BootstrapSecrets (by secret id) the deploy
// mints when the bootstrap source has none: only under a
// GCPSecretManager source, and only the seal key and the Postgres
// password.
BootstrapGenerate map[string]release.RenderedGeneratedSecret `json:"bootstrap_generate,omitempty"`
// Bootstrap is the delivery the component renders under a
// GCPSecretManager source: nil under any other.
Bootstrap *OpenBaoBootstrapEntity `json:"bootstrap,omitempty"`
}
OpenBaoEntity is the rendered `openbao` block (kcl/lib/openbao.k): where the store is and how a client logs in to it. Declarations only; no value.
type OptionSpec ¶ added in v0.1.45
type OptionSpec struct {
Name string `json:"name"`
Type string `json:"type"`
Effect string `json:"effect"`
Default string `json:"default"`
}
OptionSpec is one proto option field on (forge.v1.service) or (forge.v1.method). Name/Type are read from the compiled descriptor.
type RemoteBuild ¶ added in v0.1.45
type RemoteBuild struct {
OutputName string `json:"output_name,omitempty"`
Source RemoteBuildSource `json:"source"`
Dockerfile string `json:"dockerfile,omitempty"`
Target string `json:"target,omitempty"`
Platform string `json:"platform,omitempty"`
BuildArgs map[string]string `json:"build_args,omitempty"`
CPUMillicores int64 `json:"cpu_millicores,omitempty"`
MemoryBytes int64 `json:"memory_bytes,omitempty"`
CacheGiB int32 `json:"cache_gib,omitempty"`
TimeoutSeconds int32 `json:"timeout_seconds,omitempty"`
}
RemoteBuild mirrors the kcl/schema.k RemoteBuild — an image built by a HOSTED build service rather than on the machine running `forge build`.
It is the only member of this union that does NOT build locally, and every other difference follows from that. Source is a repo+ref PIN rather than a path, because a build service cannot read the caller's filesystem. The build is ASYNCHRONOUS, because it may queue behind other work — every other build in forge is synchronous, which is worth knowing before wiring one into a pipeline that assumes otherwise.
What is absent is the security model, and it is absent by design because a hosted service executes the caller's instructions on shared infrastructure: no registry, no push credential, no cache reference, no tag, and no secret build args. The service derives the destination and the cache from the identity it AUTHENTICATED the submission with, tags by resolved commit, and holds the registry credential where the build never reaches it. See the schema docstring for the argument behind each omission.
type RemoteBuildSource ¶ added in v0.1.45
type RemoteBuildSource struct {
Repo string `json:"repo"`
Ref string `json:"ref"`
Subdir string `json:"subdir,omitempty"`
}
RemoteBuildSource mirrors the kcl/schema.k GitSource as it appears inside a RemoteBuild — the repo+ref pin the build is performed against.
Ref is REQUIRED and Commit is absent on purpose: KCL declares the pin a human writes (a tag, a branch, a sha), and resolving it to an immutable commit is the submitting client's job, not the schema's. A KCL field holding a resolved sha would go stale the moment the branch moved, while looking authoritative.
type RenderedSecretEntity ¶
type RenderedSecretEntity struct {
Name string `json:"name"`
Keys map[string]RenderedSecretKeyEntity `json:"keys"`
// Cluster / Namespace are the EXPLICIT placement (kubectl context +
// namespace). Always set for a Bundle.rendered_secrets entry. On a
// RenderedSecrets provider entry an empty Cluster means "infer": land
// in each cluster whose services reference it by secret_ref.
Cluster string `json:"cluster,omitempty"`
Namespace string `json:"namespace,omitempty"`
}
RenderedSecretEntity mirrors the kcl/schema.k RenderedSecret — one k8s Secret forge renders from declared sources. Keys maps each in-Secret key to its value source.
type RenderedSecretKeyEntity ¶
type RenderedSecretKeyEntity struct {
From string `json:"from"`
Key string `json:"key,omitempty"`
Value string `json:"value,omitempty"`
}
RenderedSecretKeyEntity mirrors the kcl/schema.k RenderedSecretKey. From is "dotenv" (read .env.<env> at Key) or "literal" (inline Value, dev/e2e only). A "dotenv" key carries no Value; a "literal" key carries no dotenv Key.
type RouteHealthCheckEntity ¶ added in v0.1.45
type RouteHealthCheckEntity struct {
Path string `json:"path"`
ExpectedStatuses []int `json:"expected_statuses,omitempty"`
Interval string `json:"interval,omitempty"`
Timeout string `json:"timeout,omitempty"`
UnhealthyThreshold int `json:"unhealthy_threshold,omitempty"`
HealthyThreshold int `json:"healthy_threshold,omitempty"`
}
RouteHealthCheckEntity mirrors kcl/schema.k's RouteHealthCheck — an ACTIVE health check. ExpectedStatuses is the field that matters: a token-authed registry answers /v2/ with 401 when healthy, so a default 2xx check would mark every replica down.
type RouteOutlierDetectionEntity ¶ added in v0.1.45
type RouteOutlierDetectionEntity struct {
Consecutive5xx int `json:"consecutive_5xx,omitempty"`
ConsecutiveGatewayErrors int `json:"consecutive_gateway_errors,omitempty"`
Interval string `json:"interval,omitempty"`
BaseEjectionTime string `json:"base_ejection_time,omitempty"`
MaxEjectionPercent int `json:"max_ejection_percent,omitempty"`
}
RouteOutlierDetectionEntity mirrors kcl/schema.k's RouteOutlierDetection — PASSIVE health checking, which ejects an endpoint that is failing real requests.
type RouteRetriesEntity ¶ added in v0.1.45
type RouteRetriesEntity struct {
On []string `json:"on"`
Attempts int `json:"attempts"`
PerTryTimeout string `json:"per_try_timeout,omitempty"`
}
RouteRetriesEntity mirrors kcl/schema.k's RouteRetries — how a route retries a failed attempt at its backend. On is Envoy Gateway's closed trigger set (5xx, reset, connect-failure, …).
type RouteTrafficEntity ¶ added in v0.1.45
type RouteTrafficEntity struct {
Retries *RouteRetriesEntity `json:"retries,omitempty"`
Timeout string `json:"timeout,omitempty"`
HealthCheck *RouteHealthCheckEntity `json:"health_check,omitempty"`
OutlierDetection *RouteOutlierDetectionEntity `json:"outlier_detection,omitempty"`
}
RouteTrafficEntity mirrors kcl/schema.k's RouteTraffic — the typed per-route policy that renders ONE Envoy Gateway BackendTrafficPolicy. Timeout is the whole-request timeout (a multi-GB blob upload runs far past Envoy's 15s default).
type RuntimeEntity ¶ added in v0.1.45
type RuntimeEntity struct {
Type string
Host *HostRuntime
Compose *ComposeRuntime
Cluster *ClusterRuntime
Hosted *HostedRuntime
BuildOnly *BuildOnlyDeploy
}
RuntimeEntity is a workload's resolved runtime. Type carries the tag; exactly one variant pointer is non-nil, except for "hosted", whose runtime has no fields (the control plane owns every placement fact).
func (RuntimeEntity) HostedPlatform ¶ added in v0.1.45
func (r RuntimeEntity) HostedPlatform() string
HostedPlatform is the GOARCH a hosted workload's image must be built for.
type SecretProviderEntity ¶
type SecretProviderEntity struct {
Type string `json:"type"`
Path string `json:"path,omitempty"`
// Secrets is populated for Type=="rendered": the explicit Secret
// declarations (name + per-key source) forge renders + applies per
// cluster. Empty for dotenv/external.
Secrets []RenderedSecretEntity `json:"secrets,omitempty"`
// Generate is populated for Type=="file": store key -> how to mint it
// when absent (FileSecrets.generate). Never set for any other provider.
Generate map[string]secrets.GenerateSpec `json:"generate,omitempty"`
}
SecretProviderEntity is the parsed bundle-level secret provider declaration. Type is "dotenv" | "external" | "rendered". Path is the dotenv path (dotenv only), resolved relative to the project root by the CLI. Secrets is the declared Secret set (rendered only).
type ShellBuild ¶
type ShellBuild struct {
OutputName string `json:"output_name,omitempty"`
Cmd string `json:"cmd"`
Cwd string `json:"cwd,omitempty"`
Source *config.GitSource `json:"source,omitempty"`
// SourceDir is invocation-local; the declaration stays intact for provenance.
SourceDir string `json:"-"`
Env map[string]string `json:"env,omitempty"`
// BuiltImages maps an env var the command reads to the workload whose
// built image it carries. The build runs each named workload first and
// sets the var to the `<repository>@sha256:<digest>` it pushed — see
// externalBuildGraph.
BuiltImages map[string]string `json:"built_images,omitempty"`
}
ShellBuild mirrors the kcl/schema.k ShellBuild — the SINGLE shell escape hatch: a verbatim `sh -c` build command that owns the whole build (and any push).
Execution contract (see internal/buildtarget):
- cwd == Cwd resolved against the project root (relative paths join the dir holding forge.yaml; absolute pass through). Empty Cwd => the project root, so relative paths like scripts/build-image.sh, ../sibling-repo, or docker/Dockerfile resolve as a user expects. A Cwd that doesn't exist on disk is a HARD build failure.
- Cmd is run VERBATIM. Forge substitutes nothing into it: it is a plain KCL string, so the tag, arch and env are composed in KCL (forge.image_tag(), forge.target_arch(), forge.env()) where those values already live, and every `$VAR` in the command is the shell's.
- Env vars are merged onto the command's process environment (declared keys win), which is also how to keep a `${NAME}` spelling in the command: declare NAME in Env and the shell resolves it.
- on success forge captures the pushed digest (best-effort) and writes the build-state file so deploy pins the same tag/digest.
Absorbs the former flat Service.build_cmd / build_cwd / build_env trio — one declaration surface, one contract.
type SkillAudience ¶
type SkillAudience string
SkillAudience names the consumer of a skill emission. Combined with the per-skill frontmatter `emit:` field (SkillEmit) it decides which skills are written and how their bodies are rendered:
audience=All ("") — every skill, full body (no stripping). Default.
audience=General — emit:general|both skills only; @forge-only blocks stripped.
audience=Forge — emit:forge|both skills only; full body retained.
const ( // SkillAudienceAll disables filtering entirely. Use when bulk-exporting // the canonical catalog where the // reader can decide what to surface. SkillAudienceAll SkillAudience = "" // SkillAudienceGeneral targets consumers outside a forge project. The // renderer strips `<!-- @forge-only:start/end -->` blocks from emit:both // skills, and drops emit:forge skills entirely. SkillAudienceGeneral SkillAudience = "general" // SkillAudienceForge targets consumers inside a forge project. Full // body is preserved; emit:general skills are also included. SkillAudienceForge SkillAudience = "forge" )
type SkillEmit ¶
type SkillEmit string
SkillEmit declares which audience(s) a skill is authored for. Read from the YAML frontmatter `emit:` field. Drives the dual-audience compile path: a single SKILL.md source can target general consumers, forge consumers, or both — with `<!-- @forge-only:start/end -->` blocks stripped from the body when emitting to a general audience.
An empty value is treated as SkillEmitForge by [emitMatchesAudience] — legacy skills shipped under templates/project/skills/forge/ pre-date the field and are framework-specific by default.
const ( // SkillEmitForge — framework skills (proto, db, api, etc.) that only // make sense in a forge project. The legacy default. SkillEmitForge SkillEmit = "forge" // SkillEmitGeneral — methodology skills (debug, code-review, etc.) // that apply to any project, forge or not. SkillEmitGeneral SkillEmit = "general" // SkillEmitBoth — the body has both general and framework content, // with the latter inside `@forge-only` blocks. The renderer keeps the // whole body for forge audiences and strips the blocks for general. SkillEmitBoth SkillEmit = "both" )
type SkillListOptions ¶
type SkillListOptions struct {
// IncludeMigrations opts one-time migration skills
// (relevance: migration) back into the listing. Default listings
// exclude them — they only matter while crossing a specific forge
// version transition.
IncludeMigrations bool
}
SkillListOptions tunes ListSkillsAtWithOptions. The zero value is the default-listing behavior (migration skills excluded).
type SkillMetaPublic ¶
type SkillMetaPublic struct {
Path string
Name string
Description string
Scope SkillScope
Emit SkillEmit
// Relevance classifies when the skill is worth surfacing: "" (always —
// the default) or "migration" (one-time upgrade transition; excluded
// from default listings, included via SkillListOptions.IncludeMigrations).
Relevance SkillRelevance
// Version is the migration skill's frontmatter `version:` — the release
// that introduced the breaking change — passed through verbatim for
// consumers that want to do their own version gating. A project
// applies the migration when its baseline is below this version.
// Empty for non-migration skills.
Version string
// SkillForgeVersion is the forge version the skill content ships with —
// i.e. the version of the forge module/binary serving this listing.
// Empty for project/user-scope skills (their content is user-owned and
// not tied to a forge release).
SkillForgeVersion string
// ProjectForgeVersion is the forge_version pinned in the project's
// forge.yaml ("" when no project root was given, no pin exists, or the
// config could not be read).
ProjectForgeVersion string
// VersionSkew is true when SkillForgeVersion and ProjectForgeVersion
// are both known, comparable release versions and differ — i.e. the
// skill content served here comes from a different forge version than
// the one the project was generated with.
VersionSkew bool
}
SkillMetaPublic is the cross-package view of a skill returned from ListSkillsAt. Only metadata is included; bodies are fetched separately via ResolveSkillContentAt to keep enumeration cheap.
func ListSkillsAt ¶
func ListSkillsAt(projectRoot string) ([]SkillMetaPublic, error)
ListSkillsAt is the exported wrapper around [listSkillsAt], intended for consumers in sibling packages (e.g. forge/cli's public shim). It hides the unexported skillMeta type behind a stable struct.
Migration skills (relevance: migration) are excluded — this is the DEFAULT listing surface. Use ListSkillsAtWithOptions with IncludeMigrations to opt them in.
func ListSkillsAtWithOptions ¶
func ListSkillsAtWithOptions(projectRoot string, opts SkillListOptions) ([]SkillMetaPublic, error)
ListSkillsAtWithOptions is ListSkillsAt with explicit listing options. Additive surface — ListSkillsAt's signature is frozen for reliant.
type SkillRelevance ¶
type SkillRelevance string
SkillRelevance classifies WHEN a skill is worth surfacing, read from the YAML frontmatter `relevance:` field. It is orthogonal to SkillEmit (which audience) — relevance says "is this skill applicable to the steady-state of a project, or only during a specific transition?".
An empty value means "always relevant" — the default for every skill that predates the field.
Why relevance-class gating instead of version-range gating: forge binaries very commonly run as dev builds or Go pseudo-versions (see [isForgeVersionSkew], which treats those as non-comparable), so a listing-time version comparison would silently degrade to "include everything" for most real installs. Proper per-project version-range + detection-script gating for migration skills already exists in `forge project upgrade list` (applies-from/applies-to frontmatter, upgrade_migrations.go); listings only need the coarse class. The applies-from/applies-to bounds are still parsed and exposed on the metadata so consumers (e.g. reliant) can make their own call.
const ( // SkillRelevanceMigration — one-time upgrade-transition playbooks // (skills/forge/migrations/*). Noise for any project that is already // past (or before) the transition, so DEFAULT listings and the // project-skill regeneration exclude them. They remain loadable by // exact path (`forge skill load migrations/...`) and are surfaced // with proper version-range gating by `forge project upgrade list`. SkillRelevanceMigration SkillRelevance = "migration" )
type SkillScope ¶
type SkillScope string
SkillScope identifies where a skill was discovered from.
const ( // SkillScopeForge is a skill bundled with the forge binary (templates/project/skills). SkillScopeForge SkillScope = "forge" // SkillScopeProject is a skill discovered under <project_root>/.forge/skills/. SkillScopeProject SkillScope = "project" // SkillScopeUser is a skill discovered under ~/.forge/skills/. SkillScopeUser SkillScope = "user" )
func ResolveSkillContentAt ¶
func ResolveSkillContentAt(projectRoot, skillPath string) ([]byte, SkillScope, error)
ResolveSkillContentAt is the exported wrapper around [resolveSkillContentAt], with one addition for out-of-process consumers: when the skill is forge-shipped and the running forge version differs from the project's pinned forge_version, a one-line advisory is prepended to the body (after the YAML frontmatter) so the reader knows the guidance may not match the project's generated code.
type StaticSiteCDN ¶ added in v0.1.45
type StaticSiteCDN struct {
URLMap string `json:"url_map"`
Invalidate string `json:"invalidate,omitempty"`
ExtraInvalidatePaths []string `json:"extra_invalidate_paths,omitempty"`
}
StaticSiteCDN is the CDN in front of a bucket and the per-deploy invalidation policy. Absent (nil) means the bucket is served directly and a deploy invalidates nothing.
type StoreSourceEntity ¶ added in v1.0.0
type StoreSourceEntity struct {
release.RenderedStoreSource
// Templates are the Secret keys ESO renders from the fetched data.
Templates map[string]string `json:"templates,omitempty"`
// CreationPolicy is "Orphan" or "Merge": whether ESO rebuilds the Secret's
// data from the declared keys only, or merges into what is there.
CreationPolicy string `json:"creation_policy,omitempty"`
// SecretType is the Kubernetes Secret type ESO gives the synced Secret
// when it is not Opaque ("kubernetes.io/basic-auth").
SecretType string `json:"secret_type,omitempty"`
// Labels are put on the synced Secret.
Labels map[string]string `json:"labels,omitempty"`
// Shadow syncs into <name>-eso-shadow instead of <name> (migration step 2).
Shadow bool `json:"shadow,omitempty"`
// Cluster is the kubectl context the Secret lands on when the source
// names one; empty means the env's primary cluster.
Cluster string `json:"cluster,omitempty"`
}
StoreSourceEntity is a rendered forge.StoreSource: release's own record (store, data, generate, pin, immutable) plus the fields the secret commands read that the release shape does not carry.
type ValidateRuleSpec ¶ added in v0.1.45
type ValidateRuleSpec struct {
Rule string `json:"rule"`
AppliesTo []string `json:"applies_to"`
Example string `json:"example"`
DBEffect string `json:"db_effect"`
ZodEffect string `json:"zod_effect"`
WireEffect string `json:"wire_effect"`
}
ValidateRuleSpec is one projected protovalidate rule and its three enforcement points. Rule is the leaf rule name as protovalidate declares it (the name the author types: `min_len`, `len`, `gte`), AppliesTo the proto scalar kinds the projection actually fires on, and Example the exact option text that produced DBEffect/ZodEffect. Those two are SAMPLE projections computed by forge's own FieldConstraints methods (column named "value"); the real literal follows the bound the author declared.
type WebRuntimeModule ¶ added in v0.1.45
type WebRuntimeModule struct {
Name string `json:"name"` // e.g. "interceptors" (the ./interceptors source module)
Symbols []string `json:"symbols"` // exported identifiers, in barrel order
Synopsis string `json:"synopsis,omitempty"` // first sentence of the barrel's comment for the group
}
WebRuntimeModule is one module the runtime's barrel re-exports: the symbols it contributes and the barrel's own comment about it.
type WebRuntimeSpec ¶ added in v0.1.45
type WebRuntimeSpec struct {
Package string `json:"package"`
Version string `json:"version,omitempty"`
Dir string `json:"dir,omitempty"`
Description string `json:"description,omitempty"`
EntryPoints []string `json:"entry_points,omitempty"` // import specifiers from the exports map
Modules []WebRuntimeModule `json:"modules,omitempty"`
// Declared is the specifier the frontend's package.json asks for, kept
// even when the package is not on disk so the reader can tell a dev
// `file:` bridge from a published range.
Declared string `json:"declared,omitempty"`
// Reason explains an empty Dir rather than leaving the caller to guess
// whether the runtime exists at all.
Reason string `json:"reason,omitempty"`
}
WebRuntimeSpec is the frontend-runtime inventory. Dir is empty when the package is declared but not installed; Reason then says why.
type WorkloadEntity ¶ added in v0.1.45
type WorkloadEntity struct {
Name string
// Kind is the workload kind (service|worker|job|cron|operator|tool). It
// always equals Spec.EffectiveKind().
Kind string
// Image is the registry-less artifact name forge builds this workload
// into ("" = no artifact of its own). It is the build-state / release
// ledger key. Spec.Image is the RESOLVED reference a runtime pulls.
Image string
// BuildImage is Image plus the tag the workload's own image pins
// ("workspace-base:dev-per-daemon"), else Image. It is the BUILD's
// identity and so independent of the runtime: a BuildOnly or host
// workload resolves no Spec.Image, and its pin must still reach
// `forge build`. Read it through PinnedBuildTag.
BuildImage string
// Build is the typed build declaration. Type=="" means forge does not
// build this workload (a third-party image, a compose service, a
// sibling binary): there is no synthesized default.
Build BuildConfigEntity
// Runtime is where the workload runs, resolved (w.runtime or the
// bundle's default). Exactly one of its variant pointers is set.
Runtime RuntimeEntity
// FlowChecks are the app-flow health endpoints this workload serves
// (kcl/workload.k FlowCheck), probed by `forge env smoke`.
FlowChecks []FlowCheckEntity
// Spec is the runtime-independent declaration, decoded STRICTLY into
// forge's wire type — the same bytes a hosted Workload CR carries.
Spec deployv1alpha1.WorkloadSpec
// DeliveryUnit is the delivery unit the workload's objects belong to ("" =
// the default unit). Authoring-only: it never reaches the wire spec.
DeliveryUnit string
ReleaseDependencies map[string]string
ReleaseInputs map[string]string
// Requires names the workloads this one is only useful beside; it is
// skipped when `forge env up` skips an optional one of them
// (Workload.requires in kcl/workload.k).
Requires []string
}
WorkloadEntity is one runnable thing (ADR 0002): a service, worker, job, cron, operator or tool, declared once as fw.Workload and bound to ONE runtime. The same entity drives every consumer — `forge build` reads Build, `forge env up` derives the host argv from Build + Spec.Args, the deploy dispatcher groups by Runtime, and a Hosted workload's Spec is published verbatim as a forge.dev Workload CR.
func (WorkloadEntity) EffectiveBuildCmd ¶ added in v0.1.45
func (w WorkloadEntity) EffectiveBuildCmd() string
EffectiveBuildCmd is the shell build command, or "" for a workload that is not shell-built (the external-build dispatcher's "skip" signal).
func (WorkloadEntity) EffectiveBuildCwd ¶ added in v0.1.45
func (w WorkloadEntity) EffectiveBuildCwd() string
EffectiveBuildCwd is the shell build's working directory (empty => the project root). "" for a non-shell build.
func (WorkloadEntity) EffectiveBuildEnv ¶ added in v0.1.45
func (w WorkloadEntity) EffectiveBuildEnv() map[string]string
EffectiveBuildEnv is the env merged into the shell build command's environment and substitution map. nil for a non-shell build.
func (WorkloadEntity) EffectiveBuiltImages ¶ added in v1.0.0
func (w WorkloadEntity) EffectiveBuiltImages() map[string]string
EffectiveBuiltImages is the shell build's built_images edges (env var -> workload). nil for a non-shell build.
func (WorkloadEntity) EnvVars ¶ added in v0.1.45
func (w WorkloadEntity) EnvVars() []KCLEnvVar
EnvVars projects the workload's spec.env onto forge's KCLEnvVar channels, so the secret preflight, namespace guards and host env layering read one shape. A value maps to value, a SecretRef to secret_ref/secret_key, and a ConfigMapRef to config_map_ref/config_map_key.
ManagedSecret, DatabaseRef, WorkloadURL and FieldRef are NOT projected, deliberately. ManagedSecret and DatabaseRef read Secrets the env's secret store does not render by name (forge-managed-secrets is materialized from the managed store, "<db>-app" is published by CloudNativePG), so pre-flighting them as store keys would report each as missing. An unresolved WorkloadURL has no value yet, and a FieldRef exists only inside a pod.
func (WorkloadEntity) GoBuild ¶ added in v0.1.45
func (w WorkloadEntity) GoBuild() *GoBuild
GoBuild returns the workload's GoBuild, or nil.
func (WorkloadEntity) HostEnv ¶ added in v0.1.45
func (w WorkloadEntity) HostEnv() map[string]string
HostEnv is the literal env a HOST launch of this workload receives from its declaration: every spec.env value channel, with the runtime's per-run LaunchEnv layered on top. Secret channels resolve separately (scoped through the secret provider), and a reference with no host literal (ConfigMapRef, FieldRef, DatabaseRef, ManagedSecret, WorkloadURL) has nothing to contribute here.
func (WorkloadEntity) HostPort ¶ added in v0.1.45
func (w WorkloadEntity) HostPort() int
HostPort is the workload's canonical (summary / URL) host port: the first declared listen port, else its `http` port, else its first port. 0 when it binds nothing.
func (WorkloadEntity) HostPorts ¶ added in v0.1.45
func (w WorkloadEntity) HostPorts() []int
HostPorts is every TCP port a HOST launch of this workload binds, in declaration order and de-duplicated: the runtime's listen_ports when declared (an EMPTY list is the statement "binds nothing"), else the workload's spec.ports. There is no env-var heuristic: a workload declares its ports, and guessing them from *_PORT variables misread dependency addresses (TEMPORAL_PORT) as bind ports.
func (WorkloadEntity) IsJob ¶ added in v0.1.45
func (w WorkloadEntity) IsJob() bool
IsJob reports whether the workload is a one-shot job.
func (WorkloadEntity) LongRunning ¶ added in v0.1.45
func (w WorkloadEntity) LongRunning() bool
LongRunning reports whether the workload is a process that stays up (service, worker, operator) rather than one that runs to completion (job, cron) or never runs (tool).
func (WorkloadEntity) OnRuntime ¶ added in v0.1.45
func (w WorkloadEntity) OnRuntime(runtime string) bool
OnRuntime reports whether the workload is bound to the named runtime.
func (WorkloadEntity) PinnedBuildTag ¶ added in v0.1.45
func (w WorkloadEntity) PinnedBuildTag() (string, bool)
PinnedBuildTag is the tag the workload's own image pins for its build ("workspace-base:dev-per-daemon" answers dev-per-daemon). ok=false when it pins none (the env tag applies) or forge builds nothing for it.
Source Files
¶
- annotations.go
- api.go
- apply_outcome.go
- apply_record.go
- apply_record_status.go
- audit_external_builds.go
- audit_ingress_cli.go
- audit_prereqs_cli.go
- bgstart_unix.go
- binding_store.go
- bootstrap_secret.go
- build.go
- build_docker_daemon.go
- build_empty_release.go
- build_external.go
- build_external_artifacts.go
- build_external_graph.go
- build_generate.go
- build_guard.go
- build_memory.go
- build_memory_sysmem.go
- build_metrics_schema.go
- build_plan.go
- build_push_registry.go
- build_receipt.go
- build_source.go
- build_source_date_epoch.go
- build_state.go
- build_version_stamp.go
- bundle_registry_auth.go
- capabilities.go
- ci.go
- ci_gate.go
- ci_run.go
- ci_summarize.go
- ci_test_budget.go
- ci_test_run.go
- ci_verify_scratch.go
- ci_vuln_exempt.go
- ci_vuln_npm.go
- claimable_limits.go
- cloud_cmd.go
- cloud_identity.go
- cloud_identity_users.go
- cloud_token.go
- cluster_binding.go
- cluster_chart_recover.go
- cluster_cidr_drift.go
- cluster_connect.go
- cluster_connect_delivery.go
- cluster_connect_detect.go
- cluster_connect_diff.go
- cluster_connect_issuer.go
- cluster_connect_rbac.go
- cluster_connect_scope.go
- cluster_list.go
- cluster_phase.go
- cluster_provider.go
- cluster_registry.go
- cluster_repair.go
- compose_identity.go
- config.go
- config_kcl.go
- connrefused_unix.go
- db.go
- db_dirty_recovery.go
- db_migrate_kit.go
- db_migration_closed_set.go
- db_paths.go
- db_query.go
- db_query_addr.go
- db_query_cloud.go
- db_query_cluster.go
- db_query_release.go
- db_reset.go
- db_seed.go
- db_squash_dsn.go
- db_squash_dump.go
- db_squash_output.go
- db_target.go
- delete.go
- deploy.go
- deploy_all.go
- deploy_attempt.go
- deploy_autoversion.go
- deploy_claimable.go
- deploy_confirm.go
- deploy_dispatch.go
- deploy_flux_secrets.go
- deploy_helm.go
- deploy_hosted.go
- deploy_hub_ready.go
- deploy_json.go
- deploy_namespace_check.go
- deploy_order.go
- deploy_plan_applied.go
- deploy_plan_charts.go
- deploy_plan_fields.go
- deploy_plan_gate.go
- deploy_plan_secrets.go
- deploy_plan_source.go
- deploy_policy.go
- deploy_promote.go
- deploy_promote_follow.go
- deploy_promote_unpublished.go
- deploy_queued.go
- deploy_release_decision.go
- deploy_scope_record.go
- deploy_scoped.go
- deploy_secret_scope.go
- deploy_shape.go
- deploy_ship_log.go
- deploy_store_preflight.go
- deploy_target_scope.go
- detach_unix.go
- dev.go
- dev_cluster.go
- dev_cluster_ingress.go
- dev_cluster_mkcert.go
- dev_info.go
- dev_instances.go
- dev_logs.go
- dev_status.go
- dev_urls.go
- devstack.go
- devstack_activate.go
- disown.go
- disown_absent.go
- docker_preflight.go
- doctor.go
- doctor_dockerproxy.go
- doctor_external_builds.go
- doctor_ingress.go
- doctor_kclplugin.go
- doctor_parity.go
- doctor_sources.go
- doctor_tools.go
- domain_cmd.go
- env.go
- env_alerts.go
- env_build.go
- env_build_bundle.go
- env_build_observability.go
- env_config.go
- env_destination.go
- env_diff.go
- env_discovery.go
- env_exec.go
- env_history.go
- env_hosted_lifecycle.go
- env_identity.go
- env_lifecycle.go
- env_options.go
- env_probe.go
- env_releases.go
- env_releases_at.go
- env_releases_backfill.go
- env_releases_list.go
- env_releases_show.go
- env_render.go
- env_render_charts.go
- env_shape.go
- env_shape_store_secrets.go
- env_status.go
- env_status_checks.go
- env_status_deployed.go
- env_status_records.go
- env_status_release.go
- env_status_release_hosted.go
- env_topology.go
- env_up_locality.go
- env_verify.go
- env_wait.go
- env_workload_config.go
- exitcodes.go
- feature_gate.go
- features.go
- forge_bridge.go
- forge_descriptor.go
- forge_pin_delegate.go
- forge_pin_skew.go
- forge_version.go
- frontend_config_kcl.go
- frontend_config_refresh.go
- frontend_config_schema_check.go
- frontend_source.go
- frontend_sourcemaps.go
- gate_cmd.go
- gate_doc.go
- gate_producers.go
- gate_store.go
- generate.go
- generate_bootstrap.go
- generate_buf.go
- generate_buf_preflight.go
- generate_buf_ts_writers.go
- generate_buf_vendored_inputs.go
- generate_build_stamp.go
- generate_cache.go
- generate_cache_ctime_linux.go
- generate_ci.go
- generate_ci_frontends.go
- generate_ci_hosted.go
- generate_cleanup.go
- generate_config_check.go
- generate_contract_test_declines.go
- generate_crd_kcl.go
- generate_dangling_check.go
- generate_dashboard_processes.go
- generate_drift_hints.go
- generate_drift_rescue.go
- generate_explain.go
- generate_explain_drift.go
- generate_external_writes.go
- generate_forge_pin.go
- generate_frontend_config.go
- generate_frontend_dedupe.go
- generate_frontend_hooks.go
- generate_frontend_inventory.go
- generate_frontend_mocks.go
- generate_frontend_nav.go
- generate_frontend_pages.go
- generate_frontend_renamed_imports.go
- generate_frontend_runtime.go
- generate_frontend_seams.go
- generate_frontend_tsconfig_peers.go
- generate_helpers.go
- generate_hooks.go
- generate_kcl_loadable.go
- generate_legacy_migrate.go
- generate_middleware.go
- generate_missing_scaffold.go
- generate_obs_funnels.go
- generate_openapi.go
- generate_orm.go
- generate_pipeline.go
- generate_pkg_compat.go
- generate_plan.go
- generate_proto_vendor.go
- generate_rename_check.go
- generate_retire_disowns.go
- generate_rollback_report.go
- generate_rollback_verify.go
- generate_serve.go
- generate_services.go
- generate_skills.go
- generate_stale_scaffold.go
- generate_tidy_cause.go
- generate_tier1_scope.go
- generate_tier_migrate.go
- generate_timings.go
- generate_toolchain_diagnosis.go
- generate_toolpins.go
- generate_tools.go
- generate_ts_plugin_lock.go
- generate_validate.go
- generate_validate_compile.go
- generate_verbosity.go
- generate_watch.go
- githooks.go
- graph.go
- groups.go
- help_dev.go
- hosted_apply.go
- hosted_bundle.go
- hosted_cache.go
- hosted_capacity.go
- hosted_env_resolver.go
- hosted_image_base_check.go
- hosted_import.go
- hosted_ledger.go
- hosted_live.go
- hosted_org.go
- hosted_plan.go
- hosted_platform_guard.go
- hosted_project.go
- hosted_push_base.go
- hosted_records.go
- hosted_registry_login.go
- hosted_release.go
- hosted_session.go
- hosted_snapshot.go
- hosted_static_build.go
- image_tag.go
- introspect.go
- json_envelope.go
- json_error_facts.go
- kcl_eval.go
- kcl_frontend_deploy.go
- kcl_frontend_outputs.go
- kcl_imports.go
- kcl_render.go
- kcl_render_purity.go
- kcl_subprocess.go
- kcl_vendor.go
- kubeconfig_lock.go
- kubeconfig_secret.go
- kubeconfig_serviceaccount.go
- ledger_cmd.go
- ledger_export.go
- ledger_freshness.go
- ledger_history.go
- ledger_import_source.go
- ledger_override.go
- ledger_show.go
- ledger_unimported.go
- ledger_where.go
- libraries.go
- libraries_signatures.go
- libraries_webruntime.go
- local_secrets.go
- lockfile.go
- lockfile_unix.go
- map.go
- map_helpers.go
- mcp.go
- mcp_tools.go
- memory.go
- merge_driver_install.go
- merge_driver_pin.go
- merge_generated.go
- migrate.go
- migrate_import.go
- new.go
- new_env.go
- new_env_bind.go
- new_inplace.go
- new_inplace_forge_pin.go
- new_link_forge_hint.go
- new_scaffold_marker.go
- new_site.go
- obs_alerts.go
- obs_alerts_run.go
- obs_changes.go
- obs_check.go
- obs_cloud.go
- obs_common.go
- obs_compose.go
- obs_dashboards.go
- obs_dashboards_add.go
- obs_endpoints.go
- obs_issue.go
- obs_issue_discussion.go
- obs_issues.go
- obs_logs.go
- obs_metrics.go
- obs_metrics_describe.go
- obs_metrics_exemplars.go
- obs_metrics_panel.go
- obs_metrics_query.go
- obs_profile.go
- obs_query.go
- obs_session.go
- obs_sessions.go
- obs_status.go
- obs_target.go
- obs_trace.go
- obs_traces.go
- obs_workloads.go
- offline_ledger.go
- package.go
- placement.go
- platform_charts.go
- portlistener_linux.go
- procgroup_unix.go
- procinspect_linux.go
- procowner_logic.go
- project.go
- project_checkouts.go
- project_frontends.go
- project_migrate_tdd.go
- promote_cas.go
- promote_from.go
- promote_gates.go
- promote_plan.go
- protoc_gen_orm.go
- reconcile.go
- registry_cmd.go
- release.go
- release_artifacts.go
- release_bundle_cmd.go
- release_bundle_of_record.go
- release_cmd.go
- release_commits.go
- release_compose.go
- release_compose_checkout.go
- release_compose_declaration.go
- release_compose_deploy.go
- release_compose_frontend.go
- release_compose_inputs.go
- release_compose_outputs.go
- release_compose_plan.go
- release_compose_receipt.go
- release_compose_run.go
- release_compose_start.go
- release_compose_status.go
- release_ledger_prerelease.go
- release_migrations.go
- release_verify.go
- release_version_free.go
- release_where.go
- report_bug.go
- rescaffold.go
- root.go
- route_backend_prereq.go
- run_identity.go
- run_seed.go
- scaffold_format.go
- scaffolded.go
- secret_hosted.go
- secret_import.go
- secret_store_cmd.go
- secret_store_source.go
- secret_store_source_gsm.go
- secret_store_source_kube.go
- secret_store_source_openbao.go
- secrets_cmd.go
- secrets_verify.go
- secrets_wiring.go
- session_reporter.go
- shapes.go
- skill.go
- skill_list_view.go
- skills.go
- slot.go
- smoke.go
- smoke_classify.go
- smoke_dev.go
- smoke_flow.go
- smoke_hosted.go
- source_pin.go
- start.go
- storage.go
- storage_converge.go
- storage_schedule.go
- storage_schedule_wintask.go
- subprocess_env.go
- tcp_table.go
- test_helpers.go
- test_removed.go
- tools.go
- tools_pins.go
- tree_status.go
- unit_convergence.go
- up.go
- up_air.go
- up_alerting.go
- up_clickstack.go
- up_cluster_demand.go
- up_compose_rows.go
- up_env_compose.go
- up_freshness.go
- up_identity.go
- up_jobs.go
- up_logrotate.go
- up_optional_compose.go
- up_options.go
- up_readiness.go
- up_reclaim.go
- up_session.go
- up_stacks.go
- up_status_registry.go
- up_target_infra.go
- upgrade.go
- upgrade_advisory.go
- upgrade_codemod.go
- upgrade_converge.go
- upgrade_detail.go
- upgrade_migrations.go
- upgrade_modules.go
- upgrade_report.go
- wire_int64.go
Directories
¶
| Path | Synopsis |
|---|---|
|
Package audit holds the `forge project audit` command group — a comprehensive snapshot of project state designed to orient an LLM (or human) without forcing them to grep ten different directories.
|
Package audit holds the `forge project audit` command group — a comprehensive snapshot of project state designed to orient an LLM (or human) without forcing them to grep ten different directories. |
|
Package audittype holds the small, neutral value types shared by the `forge project audit` command group (internal/cli/audit) and the internal/cli code that contributes audit categories it cannot compute without package-cli internals (the KCL-entity-typed ingress / external-builds categories, and friction.go's auditFriction).
|
Package audittype holds the small, neutral value types shared by the `forge project audit` command group (internal/cli/audit) and the internal/cli code that contributes audit categories it cannot compute without package-cli internals (the KCL-entity-typed ingress / external-builds categories, and friction.go's auditFriction). |
|
Package cmdutil holds cross-cutting helpers shared by forge's own CLI across MORE THAN ONE command group (internal/cli and its dir-nested subpackages).
|
Package cmdutil holds cross-cutting helpers shared by forge's own CLI across MORE THAN ONE command group (internal/cli and its dir-nested subpackages). |
|
Package component holds the `forge component` command group — list, search, and install UI components from forge's built-in component library.
|
Package component holds the `forge component` command group — list, search, and install UI components from forge's built-in component library. |
|
Package debug holds the `forge debug` command group — a Delve-backed interactive debugger driver (start / break / continue / eval / ...).
|
Package debug holds the `forge debug` command group — a Delve-backed interactive debugger driver (start / break / continue / eval / ...). |
|
Package factory carries the shared dependency set ("the factory") threaded through forge's own CLI command tree, plus the command REGISTRY that lets dir-nested command-group subpackages (internal/cli/scaffold, internal/cli/lint, ...) attach to the root without a group↔root import cycle.
|
Package factory carries the shared dependency set ("the factory") threaded through forge's own CLI command tree, plus the command REGISTRY that lets dir-nested command-group subpackages (internal/cli/scaffold, internal/cli/lint, ...) attach to the root without a group↔root import cycle. |
|
Package fix holds the `forge fix` command group: codemods that rewrite the project's own code to what a lint asks for, run on request.
|
Package fix holds the `forge fix` command group: codemods that rewrite the project's own code to what a lint asks for, run on request. |
|
Package lint holds the `forge lint` command group — the project linter pipeline (golangci / buf / frontend / forge-convention / scaffold / migration-safety / wire-coverage …) plus the targeted single-rule flags and the --json aggregator.
|
Package lint holds the `forge lint` command group — the project linter pipeline (golangci / buf / frontend / forge-convention / scaffold / migration-safety / wire-coverage …) plus the targeted single-rule flags and the --json aggregator. |
|
Package scaffold holds the `forge scaffold` command — the one verb that writes new code into a forge project, at two granularities picked by ARITY:
|
Package scaffold holds the `forge scaffold` command — the one verb that writes new code into a forge project, at two granularities picked by ARITY: |