Documentation
¶
Overview ¶
internal/config is a pure schema/data/constants package: it declares the canonical forge.yaml types plus package-level lookup tables (e.g. ExperimentalFeatureNames — the stable display order shared by `forge project audit`, the startup warning, and `forge project features`). The directive opts it out of the contract lint rules — specifically the exported-vars rule, which would otherwise demand ExperimentalFeatureNames become a getter. A getter is the ideal fix but its call sites reach into internal/cli (an ordered slice spread as `config.ExperimentalFeatureNames...`); converting it there is a cross-package change outside this pass. This is the accepted suppression for a genuine data/catalogue package.
The directive is a FULL opt-out, not a lint-only one: it also stops contract codegen, so the narrow Service seam in contract.go gets no generated mock. That is the intended trade here — nothing mocks config — but it is the whole cost, so weigh it before adding the directive to a package whose mock anyone compiles against.
Package config defines the canonical forge.yaml types shared across forge.
The data types (ProjectConfig, ComponentConfig, etc.) are proto-like data carriers — they ship with accessor methods (Effective*, Is*) directly on the struct values. Those methods are part of the data, not behavior to mock. The Service interface below is the package's narrow behavior surface (currently kind classification); future YAML load/save behavior moves behind it as it lands (Load lives today in internal/cli and internal/generator and is ported in those phases).
derive.go — everything forge.yaml no longer says, derived from the repo.
A scaffolded forge.yaml is minimal: name, module_path, forge_version and the few real settings. Everything else is DERIVED at load time from what exists in the tree:
- the project kind (service | cli | library) from the sources;
- every feature flag (DeriveFeatureDefaults) from the kind and from marker files and directories; the optional `features:` block pins one on or off whatever the tree says (FeaturesConfig);
- the database driver and migrations directory (DeriveDatabase) from whether db/migrations exists;
- the pnpm-workspaces layout (DeriveFrontendWorkspaces) from pnpm-workspace.yaml;
- the frontend inventory from frontends/ on disk, overlaid by the KCL `forge.Frontend` declarations (frontend_inventory.go);
- absent section blocks (ci, lint, deploy, k8s, database.migration_safety) are filled with the canonical scaffold defaults for the kind.
Anything the user writes in a section that still exists is taken literally; derivation never overrides a present value. The write side is symmetric: NormalizeForWrite drops values byte-identical to what derivation would produce, so a load → mutate → write round-trip keeps forge.yaml minimal.
derive_fill.go — field-level section defaulting.
Section defaulting used to be all-or-nothing: `if sectionIsZero(c.Database) { c.Database = d.Database }`. Writing ANY key inside a section therefore suppressed the default for every OTHER key in it, and the fields left empty were not neutral — they fed feature derivation.
The measured cost: forge's own migration-safety error recommends
database:
migration_safety:
allowed_destructive: [db/migrations/0009_harden.up.sql]
as its escape hatch. That block makes the section non-zero, so Driver stays "", DeriveFeatureDefaults reads hasDB off it and resolves FeatureMigrations to false, and `forge lint --migration-safety` answers "migrations feature is disabled in forge.yaml" and exits 0. Following forge's own instructions silently switched off the safety check the instructions were about, and a CI job running that lint would go green with nothing checked.
The fill is driven by WHICH YAML KEYS THE USER WROTE, not by whether the decoded field is zero. That distinction is the whole design: a zero-value test cannot tell `race: false` from an absent `race`, so it would resurrect every explicitly-disabled boolean whose default is true — trading a silent disable for a silent ENABLE, which is the same class of bug pointed the other way. Presence comes from the yaml.Node tree, which knows exactly.
ApplyDerivedDefaults (no node in hand — tests, post-load mutation) keeps the whole-section semantics, which is correct for its callers: they are filling a config nobody hand-wrote.
Index ¶
- Constants
- func ApplyDerivedDefaults(c *ProjectConfig)
- func ApplyDerivedDefaultsFromNode(c *ProjectConfig, root *yaml.Node)
- func DeriveDatabase(c *ProjectConfig)
- func DeriveFeatureDefaults(c *ProjectConfig) map[FeatureName]bool
- func DeriveFrontendWorkspaces(c *ProjectConfig)
- func DisabledFeatureError(name string) error
- func EffectiveProjectBinary(binary string) string
- func EffectiveProjectKind(kind string) string
- func FeatureDependencies(name FeatureName) []string
- func FrontendComponentsDir(feDir string) string
- func MatchExclude(patterns []string, pkgPath string) bool
- func NormalizeFrontendDefaults(cfg *ProjectConfig)
- func ResolveInventoryAtLoad(cfg *ProjectConfig, projectDir string)
- func SetConfigWarningSink(w io.Writer) io.Writer
- func ValidateBasePath(bp string) (string, bool)
- type APIConfig
- type BuildContext
- type CIConfig
- type CIE2EConfig
- type CIGateCheck
- type CIGateConfig
- type CIGateProbe
- type CILintConfig
- type CIPermConfig
- type CITestConfig
- type CITestSkipAllow
- type CITestSkipsConfig
- type CIVulnConfig
- type CIVulnExemption
- type CRDConfig
- type ComponentConfig
- type ConfigGuardConfig
- type ConstraintKind
- type ContractsConfig
- type DatabaseConfig
- type DeployConcurrency
- type DeployConfig
- type DeployEnvConfig
- type Deps
- type DevStackConfig
- type DockerConfig
- type FeatureName
- type FeaturesConfig
- func (f FeaturesConfig) BuildEnabled() bool
- func (f FeaturesConfig) CIEnabled() bool
- func (f FeaturesConfig) CodegenEnabled() bool
- func (f FeaturesConfig) ContractsEnabled() bool
- func (f FeaturesConfig) DeployEnabled() bool
- func (f FeaturesConfig) EffectiveFeatures() map[string]bool
- func (f FeaturesConfig) FollowingRequirements() FeaturesConfig
- func (f FeaturesConfig) FrontendEnabled() bool
- func (f FeaturesConfig) HotReloadEnabled() bool
- func (f FeaturesConfig) IngressEnabled() bool
- func (f FeaturesConfig) MigrationsEnabled() bool
- func (f FeaturesConfig) ORMEnabled() bool
- func (f FeaturesConfig) ObservabilityEnabled() bool
- func (f FeaturesConfig) OperatorsEnabled() bool
- func (f FeaturesConfig) Pin(name FeatureName, on bool) (_ FeaturesConfig, ok bool)
- func (f FeaturesConfig) Pinned(name FeatureName) bool
- func (f FeaturesConfig) With(name FeatureName, on bool) FeaturesConfig
- type FrontendConfig
- func (f FrontendConfig) ComponentsDirRel() string
- func (f FrontendConfig) DeclaredDir() string
- func (f FrontendConfig) Dir(projectDir string) (string, bool)
- func (f FrontendConfig) EffectiveDevRunner() string
- func (f FrontendConfig) EffectiveOutput() string
- func (f FrontendConfig) HasGitSource() bool
- func (f FrontendConfig) IsNextJS() bool
- func (f FrontendConfig) RoutesNone() bool
- func (f FrontendConfig) StaticExport() bool
- func (f FrontendConfig) UnknownHooks(known map[string]bool) []string
- func (f FrontendConfig) WantsHooksFor(name, fullName string) bool
- func (f FrontendConfig) WithDir(dir string) FrontendConfig
- type FrontendLintConfig
- type GenerateConfig
- type GenerateHook
- type GitSource
- type K8sConfig
- type KCLFrontend
- type LintConfig
- type LintSkipAllowance
- type LogWrapperConfig
- type MigrationSafetyConfig
- type ORMConfig
- type ObservabilityConfig
- type PlanEntity
- type PlanEntityConstraint
- type PlanEntityField
- type PlanField
- type PlanFile
- type PlanFrontend
- type PlanPackage
- type PlanRPC
- type PlanService
- type ProjectConfig
- func (c ProjectConfig) EffectiveBinary() string
- func (c ProjectConfig) EffectiveForgeVersion() string
- func (c ProjectConfig) EffectiveKind() string
- func (c *ProjectConfig) HasGoModule() bool
- func (c ProjectConfig) HasReactNativeFrontend() bool
- func (c ProjectConfig) IsBinaryShared() bool
- func (c ProjectConfig) IsCLIKind() bool
- func (c ProjectConfig) IsFrontendWorkspacesEnabled() bool
- func (c ProjectConfig) IsLibraryKind() bool
- func (c ProjectConfig) IsServiceKind() bool
- func (c *ProjectConfig) ToolchainFrontends() []FrontendConfig
- func (c ProjectConfig) WithFrontendWorkspaces(on bool) ProjectConfig
- type Service
- type TracingLintConfig
- type UpgradeConfig
- type ValidationError
- type VendoredContract
Constants ¶
const ( ProjectKindService = "service" ProjectKindCLI = "cli" ProjectKindLibrary = "library" )
ProjectKind identifies the shape of a forge project. The default, "service", produces a Connect-RPC service scaffold (handlers, middleware, deploy manifests). "cli" produces a Cobra-based CLI binary with no server-shaped scaffolding. "library" produces a pure Go module with no cmd/ entry point.
const ( ProjectBinaryPerService = "per-service" )
ProjectBinary describes the binary packaging shape for a service project. "per-service" (the default) emits the canonical layout: one `cmd/server.go` cobra root with `server [services...]` filtering at the runtime layer, and one Application per service in deploy/ KCL. "shared" emits one cobra subcommand per service so callers can invoke `./project <svc>` directly, and KCL emits a single MultiServiceApplication (one image, N Deployments) instead of N Applications. See FORGE_BACKLOG.md "Layer B" for tradeoffs.
const ( // DefaultMaxStacks — the default checkout plus seven linked worktrees. // Chosen to match the block range control-plane's k3d.yaml already // pre-mapped by hand, so adopting the ceiling changes no behavior. DefaultMaxStacks = 8 // DefaultBlockSize is the historical hardcoded quantum. DefaultBlockSize = 100 )
Dev-stack allocation defaults, applied when the dev_stack: block (or a key in it) is absent.
const ( ComponentKindServer = "server" ComponentKindWorker = "worker" ComponentKindCron = "cron" ComponentKindJob = "job" ComponentKindOperator = "operator" ComponentKindBinary = "binary" )
Component kind constants. Kind is the single discriminator on a ComponentConfig — it replaces the old `services[].type` + `services[].kind` pair and the separate `binaries:` block.
- server — Connect-RPC handlers + client + frontend hooks + bootstrap row + cobra subcommand (was type=go_service).
- worker — in-process ContextWorker goroutine; bootstrap Workers row.
- cron — scheduled job; Schedule drives it. In-process scheduled goroutine for dev, CronJob in deploy. First-class (was worker + kind:cron).
- job — ONE-SHOT: runs a command to completion, exits 0, and gates the components named in Before. The portable "do this once, then proceed" primitive (k8s init container / compose service_completed_successfully / host run-and-wait).
- operator — controller-runtime manager + CRDs.
- binary — standalone cobra subcommand cmd/<name>.go (one image, run `./app <name>`); no bootstrap wiring (was the binaries: block).
const ( // FrontendOutputStatic is a static export into out/ (`output: "export"`). FrontendOutputStatic = "static" // FrontendOutputStandalone is a self-contained Node server // (`output: "standalone"`). FrontendOutputStandalone = "standalone" // FrontendOutputServer is full Next.js (`next start`, no `output:`). FrontendOutputServer = "server" // FrontendOutputScaffoldDefault is what a NEW Next.js frontend is // scaffolded with, and written into forge.yaml. Static because the // hosted runtime serves frontends as static sites, and because the // generated CRUD routes export cleanly — so the default ships // anywhere. It is deliberately not what an empty field means; see // EffectiveOutput. FrontendOutputScaffoldDefault = FrontendOutputStatic )
Output values for FrontendConfig.Output — the Next.js production shape.
const ( DevRunnerNPM = "npm" DevRunnerPNPM = "pnpm" DevRunnerYarn = "yarn" )
Dev-runner values for FrontendConfig.DevRunner, mirroring the KCL Frontend schema's `dev_runner`.
const ( EnforceTypedAccessOff = "off" EnforceTypedAccessWarn = "warn" EnforceTypedAccessError = "error" )
Typed-access enforcement levels for ConfigGuardConfig.EnforceTypedAccess.
- off — emit no env-reading guardrail at all.
- warn — emit the forbidigo guardrail as an ADVISORY check: violations are reported (during `forge lint` and in the generated .golangci.yml) but never fail the build. This is the default for an ABSENT `config:` block, so existing projects upgrade without a flag-day, and what `forge project new --in-place` scaffolds when it adopts an existing project, whose code already reads the environment directly.
- error — emit the forbidigo guardrail as a GATING check: violations fail `forge lint` / CI. `forge project new` scaffolds this for greenfield projects, which carry no legacy env-reading debt.
const DefaultComponentsDir = "src/components/ui"
DefaultComponentsDir is where forge's component library lives in a frontend that does not say otherwise, relative to the frontend directory.
const DefaultHandlerFileMaxLOC = 1000
DefaultHandlerFileMaxLOC is the built-in threshold used by the forgeconv-handler-file-size analyzer when the project does not set lint.handler_file_max_loc in forge.yaml. Picked at 1000 because that roughly tracks "two screens of any modern editor" plus generous buffer — files past that point materially harm review velocity and almost always benefit from the per-RPC split that `forge scaffold handler-file` is intended to support.
const DefaultLoaderPackage = "pkg/config"
DefaultLoaderPackage is the package path forge's config-loader codegen emits into a consuming project (generated from proto/<project>/config/v1/config.proto via the (forge.v1.config) annotation). It is the ONE package allowed to read the environment directly; the typed-access guardrail allowlists it so the generated loader can legitimately call os.Getenv / os.LookupEnv.
const DefaultMigrationsDir = "db/migrations"
DefaultMigrationsDir is where migrations live.
const DefaultORMPackage = "internal/db"
DefaultORMPackage is where the generated data layer lands when database.orm.package is unset.
const DefaultServePort = 8080
DefaultServePort is the ONE port fact forge itself knows: the port a scaffolded binary listens on. Every service in a binary mounts onto the same Connect mux and the process listens exactly once, on AppConfig.port (env PORT, default 8080) — so the port belongs to the BINARY, never to an individual component.
Any OTHER port is a deploy fact and lives in `deploy/kcl/<env>/main.k` (forge.workloads.Workload.ports, refined per environment). Nothing forge introspects — the proto descriptor, the owned worker/operator files, cmd/ — carries a port, so there is deliberately no per-component port field to read.
const NoORMDirective = "//forge:no-orm"
NoORMDirective is the package-level marker that opts a project out of the generated ORM. It goes in the doc comment of the hand-written DB package (internal/db), beside the code it affects:
// Package db is the hand-owned repository layer. // //forge:no-orm: the repository is hand-written; entities are not projected package db
It mirrors `//forge:exclude-contract: <why>`, the other per-package opt-out already in use: the exemption sits in the package it exempts, with a reason, where a reader of that package sees it. A forge.yaml key was the wrong home because it described a property of internal/db from a file that never mentions internal/db. The reason is not required by the loader.
const PackageForgeKey = "forge"
PackageForgeKey is the package.json key holding a frontend's forge block.
const ProjectConfigFile = "forge.yaml"
ProjectConfigFile is the fixed name of a forge project's manifest. It is declared here so every on-disk read spells it the same way.
const ProtoPackageServicePlaceholder = "{service}"
ProtoPackageServicePlaceholder is the token in api.proto_package that expands to the scaffolded service's snake_case name.
const RootFrontendName = "web"
RootFrontendName is the name a frontend at the project root is known by until a KCL declaration names it: the name `forge project new --in-place --kind site` declares it under, and the scaffold's default frontend name.
const RoutesNone = "none"
RoutesNone is the frontends[].routes value meaning "generate no CRUD pages for this frontend".
Variables ¶
This section is empty.
Functions ¶
func ApplyDerivedDefaults ¶
func ApplyDerivedDefaults(c *ProjectConfig)
ApplyDerivedDefaults resolves the shape-derived state of a freshly unmarshalled ProjectConfig:
- every section block that is entirely absent (zero value) is filled with the canonical scaffold default for the project kind;
- the features block gets its derivation context attached so the *Enabled() accessors can resolve absent flags from shape.
Called by the loader (LoadProject) — code that hand-constructs a ProjectConfig in tests without calling this keeps the historical zero-value semantics.
Prefer ApplyDerivedDefaultsFromNode when the yaml.Node is in hand: a partially-written section needs FIELD-level defaulting, and only the node knows which keys the user actually wrote. See derive_fill.go for why a zero-value test cannot substitute.
func ApplyDerivedDefaultsFromNode ¶ added in v0.1.45
func ApplyDerivedDefaultsFromNode(c *ProjectConfig, root *yaml.Node)
ApplyDerivedDefaultsFromNode is ApplyDerivedDefaults with the parsed forge.yaml mapping in hand, so an absent FIELD inside a present section still gets its derived default.
A nil root reproduces the historical whole-section behaviour: a section that is entirely zero takes the default, and a section with ANY value is left exactly as it is. With no node there is no telling which of its keys the user wrote, and a non-zero section is either theirs or already defaulted — so re-applying to a config that already went through the loader is a no-op on every section. It used to overwrite every section wholesale: the generate pipeline's frontend-inventory step re-derives a LOADED config, and that wiped database.orm.package (the ORM then projected into internal/db and the sweep deleted internal/store), the ci.vuln_scan exemptions, and every other section the user had written.
func DeriveDatabase ¶ added in v0.1.45
func DeriveDatabase(c *ProjectConfig)
DeriveDatabase fills the database driver and migrations directory from the tree: a project with db/migrations uses postgres and that directory; anything else has no database. There is no setting to write — the directory IS the declaration, the same way the schema is (the migrations are the source of truth for it).
func DeriveFeatureDefaults ¶
func DeriveFeatureDefaults(c *ProjectConfig) map[FeatureName]bool
DeriveFeatureDefaults computes the enabled/disabled state of every feature from what exists in the repo. Each feature is decided by ITS OWN evidence, not by the project kind: a deploy-only project (deploy/kcl, no protos, no server) is a service-kind project that must get deploy and nothing else.
codegen ⇔ .proto files exist (proto/ or a buf.yaml module) orm ⇔ a database AND codegen AND no //forge:no-orm marker in internal/db migrations ⇔ a database AND codegen ci ⇔ kind != library build ⇔ kind != library contracts ⇔ a Go module (contract.go works for every kind that has Go) frontend ⇔ a frontend exists (frontends/ or KCL) AND codegen observability ⇔ server sources exist (pkg/app, internal/handlers, proto/services) hot_reload ⇔ server sources exist deploy ⇔ deploy/kcl exists ingress ⇔ deploy AND deploy/kcl declares a forge.Gateway operators ⇔ internal/operators holds an operator
"a database" means db/migrations exists (DeriveDatabase).
With no on-disk project to read (a hand-built config in a test), there is no evidence to consult and the kind stands in: a service is assumed to have protos, a server and a deploy tree.
The set is dependency-consistent by construction (see feature_graph.go): every dependent is gated on the EFFECTIVE value of what it requires, so a derived set can never trip the graph validator.
func DeriveFrontendWorkspaces ¶ added in v0.1.45
func DeriveFrontendWorkspaces(c *ProjectConfig)
DeriveFrontendWorkspaces reads the pnpm-workspaces layout off the tree: a root pnpm-workspace.yaml means the project shares generated clients and hooks across frontends through packages/api and packages/hooks.
func DisabledFeatureError ¶
DisabledFeatureError returns the canonical user-facing error for a disabled feature. Centralised so every gate site emits the same wording — sub-agents and humans grepping for the string find one authoritative format. The name argument is the lowercased feature name (e.g. "deploy", "build", "frontend").
func EffectiveProjectBinary ¶
EffectiveProjectBinary returns the binary mode, defaulting to "per-service" so projects predating the field keep their existing codegen shape.
func EffectiveProjectKind ¶
EffectiveProjectKind returns the project kind, defaulting to "service" so that older forge.yaml files without a kind: field continue to behave as service projects.
func FeatureDependencies ¶
func FeatureDependencies(name FeatureName) []string
FeatureDependencies returns the feature-graph edges for name as a flat list of human-readable dependency labels (other feature names and shape preconditions). Stable order. Used by `forge project features` to print each feature's deps. Returns an empty slice for a feature with no edges.
func FrontendComponentsDir ¶ added in v1.0.0
FrontendComponentsDir is the components directory of the frontend at feDir, relative to it: its package.json components_dir, or DefaultComponentsDir when it states none (or states one forge cannot use, which generate reports).
func MatchExclude ¶
MatchExclude reports whether pkgPath matches any of the configured exclude patterns. A pattern matches only on whole path-segment boundaries — the rule for each pattern is the union of:
- equality (pattern == pkgPath)
- "/"-suffix (pkgPath ends with "/"+pattern — the `mypkg` shorthand for `internal/mypkg`, and deeper suffixes like `linter/contract`)
- "/"-prefix (pkgPath starts with pattern+"/" — excluding a directory excludes its whole subtree)
- mid-path (pkgPath contains "/"+pattern+"/" — the pattern names interior segment(s) of a longer path)
Empty patterns are skipped — see the package doc for why. Both pkgPath and patterns are normalised to forward-slash form before comparison (and a trailing "/" on a pattern is tolerated) so the helper behaves the same on every OS and on sloppy YAML input.
History: this used to be a raw strings.Contains substring rule. That was lenient on purpose (it caught the `mypkg` shorthand), but it also matched PARTIAL segments — the cp-forge project excluded `internal/auth` and silently lost codegen for its SIBLING package `internal/authutil`, because "internal/authutil" contains "internal/auth" as a substring. Crucially there was no fuller spelling that could escape the over-match: the pattern already was the full path of the package the owner wanted excluded. The segment-boundary rules above keep every legitimate match the substring rule supported (shorthand leaf, subtree, interior segments) while making "exclude X" stop meaning "exclude anything whose name merely starts with X".
func NormalizeFrontendDefaults ¶ added in v0.1.45
func NormalizeFrontendDefaults(cfg *ProjectConfig)
NormalizeFrontendDefaults fills the two per-frontend fields that are omissible in forge.yaml: `path` (the frontends/<name> convention) and `type` (nextjs).
A frontend whose code comes from another repository is left with an EMPTY path on purpose. Defaulting it to frontends/<name> would invent a directory that does not exist and never will — its real directory is whatever the source resolver materializes — and every path-consuming caller would then shell into the invention. Empty is the honest answer until the source is resolved, and it is one callers can test for.
func ResolveInventoryAtLoad ¶ added in v0.1.45
func ResolveInventoryAtLoad(cfg *ProjectConfig, projectDir string)
ResolveInventoryAtLoad fills a project's frontend inventory from the filesystem when forge.yaml declares none, and normalizes the paths and types of whatever it ends up with. It runs inside LoadProject, so EVERY command that loads a forge.yaml gets the same answer.
The load seam is the whole point. This resolution used to happen in a step registered only on the generate pipeline, which mutated the config in memory. A project that declares its frontends in KCL instead of forge.yaml was therefore generated for and then neither linted, typechecked, nor built — `forge generate` saw its frontends and every other command saw none, from the same file. Resolving where the config is LOADED is what makes one answer structurally impossible to diverge.
Only the CHEAP half lives here, and deliberately so. The marker-gated directory scan costs one readdir; deriving from KCL costs a full render per environment (measured at 1.6-2.2s each, four environments on control-plane) and would put ~7s on every `forge lint`, `forge doctor` and shell completion. The render-based half therefore stays with the commands that already render — see cli.stepDeriveFrontendInventory, which now supplements this rather than replacing it.
Best-effort by construction: a project with no frontends/ directory resolves to the empty inventory it already had, never to an error.
func SetConfigWarningSink ¶
SetConfigWarningSink overrides the destination for non-fatal config warnings and returns the previous sink so callers can restore it. Used by tests to capture warning output; production code leaves the default (os.Stderr). Swapping the sink also resets the per-process dedup set so each test starts from a clean slate.
func ValidateBasePath ¶
ValidateBasePath checks the shape of a non-empty frontend base_path value. Returns (reason, false) on failure, ("", true) when valid.
Valid: "/admin", "/internal/admin", "/v2.1_beta" Invalid: "admin" (no leading slash), "/admin/" (trailing slash),
"/" (root mount — omit the field instead), "/ad min", "/a%2Fb".
Types ¶
type APIConfig ¶
type APIConfig struct {
REST bool `yaml:"rest,omitempty"`
OpenAPI bool `yaml:"openapi,omitempty"`
// ProtoPackage is the proto `package` a SCAFFOLDED service declares
// (`forge scaffold service`, `forge project new --service`). Either a
// fixed package every service shares ("controlplane.v1") or a pattern
// carrying the {service} placeholder, which expands to the snake_case
// service name ("acme.{service}.v1" → "acme.billing.v1").
//
// Absent is the normal case: forge then infers the package from the
// project's existing service protos (a strict-majority convention) and
// falls back to "services.{service}.v1" — see
// generator.ResolveServiceProtoPackage. Set it only to override that
// inference, e.g. on a project with no services yet. Scaffold-time only:
// it never rewrites a proto that already exists. go_package is not
// affected — every service keeps its own gen/services/<svc>/v1 Go
// package whatever its wire package.
ProtoPackage string `yaml:"proto_package,omitempty"`
}
APIConfig holds project-level API protocol-skin toggles. Both fields default to false, so projects that omit the `api:` block continue to expose only the canonical Connect/gRPC handlers without any runtime transcoding or generated spec files.
REST=true installs connectrpc.com/vanguard as middleware in front of the Connect mux. Vanguard transcodes REST↔Connect at runtime based on `google.api.http` annotations on RPCs; the CRUD proto scaffolder also emits standard REST-shaped annotations on Get/List/Create/Update/Delete RPCs so the default CRUD surface gains REST URLs without hand-editing.
OpenAPI=true is owned by a sibling agent and emits an OpenAPI spec alongside the proto compile step. The two fields compose: with both on, the generated spec reflects the REST URLs.
type BuildContext ¶ added in v0.1.46
type BuildContext struct {
Path string
Source *GitSource
// SourceDir is the resolved invocation-local directory, never serialized.
SourceDir string
}
BuildContext is either a local path / BuildKit reference (a scalar), or a GitSource pin (a mapping). These alternatives cannot silently override one another. Resolution belongs to the CLI; loading config never fetches code.
func (BuildContext) MarshalJSON ¶ added in v0.1.46
func (c BuildContext) MarshalJSON() ([]byte, error)
MarshalJSON preserves the pin or path instead of serializing resolver state.
func (BuildContext) MarshalYAML ¶ added in v0.1.46
func (c BuildContext) MarshalYAML() (any, error)
MarshalYAML preserves the declaration, never the resolved local directory.
func (*BuildContext) UnmarshalJSON ¶ added in v0.1.46
func (c *BuildContext) UnmarshalJSON(data []byte) error
UnmarshalJSON decodes the same string-or-pin union emitted by KCL.
func (*BuildContext) UnmarshalYAML ¶ added in v0.1.46
func (c *BuildContext) UnmarshalYAML(node *yaml.Node) error
UnmarshalYAML decodes a string reference or a strict GitSource mapping.
func (BuildContext) Validate ¶ added in v0.1.46
func (c BuildContext) Validate() error
Validate rejects empty contexts, conflicting alternatives and malformed pins.
type CIConfig ¶
type CIConfig struct {
Provider string `yaml:"provider"`
Lint CILintConfig `yaml:"lint,omitempty"`
Test CITestConfig `yaml:"test,omitempty"`
VulnScan CIVulnConfig `yaml:"vuln_scan,omitempty"`
E2E CIE2EConfig `yaml:"e2e,omitempty"`
Permissions CIPermConfig `yaml:"permissions,omitempty"`
// TestSkips tunes `forge ci verify-test-run` — the gate that reads a
// `go test -json` stream and reports packages that skipped so much
// their pass proves nothing. Absent (the common case) means the
// shipped defaults; the block exists so a project can declare the
// packages whose heavy skipping is legitimate.
TestSkips CITestSkipsConfig `yaml:"test_skips,omitempty"`
// Gate extends `forge ci gate` — the local gate behind `task ci` and
// `task ci:checks` — with the project's own checks.
Gate CIGateConfig `yaml:"gate,omitempty"`
}
CIConfig holds CI/CD settings. Every field here is an input to the ONE-TIME workflow render: .github/workflows/*.yml is scaffold-once, user-owned (Tier-2) and `forge generate` never re-renders it, so editing this block changes nothing until the workflow file is deleted.
There is intentionally NO `go_version:` key either: every setup-go step in the generated workflows pins with `go-version-file: go.mod`, so CI tracks the project's own declared toolchain and cannot drift from it.
There is intentionally NO `extra_jobs:` key. Adding a job to a YAML file you already own is one edit in .github/workflows/ci.yml; routing it through forge.yaml was a second source of truth that the workflow generator never read (the template's `range .ExtraJobs` was fed from a struct nothing populated), so declared jobs silently vanished.
func (*CIConfig) EffectivePermContents ¶
EffectivePermContents returns the contents permission, defaulting to "read".
func (*CIConfig) IsLintEnabled ¶
IsLintEnabled returns true if any linter is enabled. Zero value (all false) is treated as "all enabled" (sensible default).
func (*CIConfig) IsTestRaceEnabled ¶
IsTestRaceEnabled returns true if the race detector should be used. Zero value is treated as enabled.
func (*CIConfig) IsVulnScanEnabled ¶
IsVulnScanEnabled returns true if any vulnerability scanner is enabled. No explicit selection is treated as "all enabled".
type CIE2EConfig ¶
type CIE2EConfig struct {
Enabled bool `yaml:"enabled"` // default false
Runtime string `yaml:"runtime,omitempty"` // "docker-compose" or "k3d"
}
CIE2EConfig controls end-to-end testing in CI.
type CIGateCheck ¶ added in v0.1.45
type CIGateCheck struct {
// Name is the row in the summary table and the name CI_SKIP and CI_ONLY
// match. Unique, and not the name of a check forge owns unless this
// check `replaces` it.
Name string `yaml:"name"`
// Command is the argv to run (no shell), from the project root unless
// Dir is set. Exit 0 is PASS.
Command []string `yaml:"command"`
// Dir is the working directory, project-relative.
Dir string `yaml:"dir,omitempty"`
// Env is added to the check's environment.
Env map[string]string `yaml:"env,omitempty"`
// Requires lists binaries that must be on PATH. A missing one makes the
// check SKIPPED — which fails the gate unless CI_SKIP names it — rather
// than a PASS for work that never ran.
Requires []string `yaml:"requires,omitempty"`
// Probe is a command that must succeed for the check to run (a docker
// daemon that answers, a sibling checkout). When it fails the check is
// SKIPPED with Probe.Reason.
Probe *CIGateProbe `yaml:"probe,omitempty"`
// SkipExitCode, when non-zero, is the exit status by which Command says
// "I could not run here" (77, the automake convention, is the usual
// choice): the row is SKIPPED with the command's last output line as the
// reason, instead of FAIL.
SkipExitCode int `yaml:"skip_exit_code,omitempty"`
// Lane is the gate lane the check belongs to: checks (the default),
// test, integration, or a lane of the project's own naming — which only
// `forge ci gate <lane>` runs, never `all`.
Lane string `yaml:"lane,omitempty"`
// Group is a label CI_ONLY can select by (`CI_ONLY=kcl`). Default:
// "extras".
Group string `yaml:"group,omitempty"`
// After names checks this one depends on: it starts once they have
// finished (whatever their verdict), and selecting it with CI_ONLY
// selects them too.
After []string `yaml:"after,omitempty"`
// Heavy makes the check hold a host-wide heavy slot while it runs, so a
// shared machine never runs more heavy jobs than it has slots for. Set
// it for whole-module builds, test runs, renders; leave it off for
// linters of a few files and network calls.
Heavy bool `yaml:"heavy,omitempty"`
// MutatesTree marks a check that writes tracked files (a formatter, a
// regenerate-and-diff): it runs alone, before every check that only
// reads the tree, and when it fails on a tree that was clean the gate
// restores the committed tree.
MutatesTree bool `yaml:"mutates_tree,omitempty"`
// Cache, when false, never reuses a cached PASS for this check: set it
// for a check whose verdict depends on state outside the tree (the
// network, a sibling checkout, today's advisories). Default true.
Cache *bool `yaml:"cache,omitempty"`
// Replaces takes over one of forge's own checks: this check runs in its
// place, in its lane and position, and forge's command does not run.
Replaces string `yaml:"replaces,omitempty"`
}
CIGateCheck is one project-owned check in `forge ci gate`.
The gate runs checks concurrently: a check waits only for its `after` dependencies, a `heavy` one also for a host-wide heavy slot (pkg/hostslot), and a `mutates_tree` one runs alone. Every check still runs when another failed.
type CIGateConfig ¶ added in v0.1.45
type CIGateConfig struct {
// Env is set for every check of the gate, forge's and the project's
// (GOPRIVATE for a private sibling module, a silencing flag).
Env map[string]string `yaml:"env,omitempty"`
// Path lists project-relative directories put first on every check's
// PATH, ahead of forge's pinned tools: a project's own pinned toolchain
// (`.tools/bin`). `requires` resolves against the same PATH.
Path []string `yaml:"path,omitempty"`
// AffectedIgnore lists files no Go test reads, as project-relative globs
// (`*.md` matches by base name anywhere, `docs/**` a whole tree): a
// change to one does not force the full unit suite when the gate tests
// only the packages a branch affects. Any other non-Go change does.
AffectedIgnore []string `yaml:"affected_ignore,omitempty"`
// ExtraChecks are the project's own rows. By default each runs in the
// `checks` lane, so the scaffolded CI job (`task ci:checks`) and a
// developer's `task ci` run the same ones.
ExtraChecks []CIGateCheck `yaml:"extra_checks,omitempty"`
}
CIGateConfig is what a project adds to the checks forge owns in `forge ci gate`. Forge's own step list (verify-generated, lint, build, vuln-scan, license-check, validate-kcl, deployability) and the versions it pins are forge's: a project turns one off with CI_SKIP, which the summary still lists, or takes one over with an extra check that `replaces` it.
type CIGateProbe ¶ added in v0.1.46
type CIGateProbe struct {
Command []string `yaml:"command"`
// Reason is the SKIPPED note when Command fails.
Reason string `yaml:"reason,omitempty"`
}
CIGateProbe is a command whose success a check needs before it runs.
type CILintConfig ¶
type CILintConfig struct {
Golangci bool `yaml:"golangci"` // default true
Buf bool `yaml:"buf"` // default true
BufBreaking bool `yaml:"buf_breaking"` // default true
Frontend bool `yaml:"frontend"` // default true
MigrationSafety bool `yaml:"migration_safety"` // default true
}
CILintConfig controls which linters run in CI.
type CIPermConfig ¶
type CIPermConfig struct {
Contents string `yaml:"contents,omitempty"` // default "read"
}
CIPermConfig controls CI workflow permissions.
type CITestConfig ¶
type CITestConfig struct {
Race bool `yaml:"race"` // default true
Coverage bool `yaml:"coverage"` // default false
}
CITestConfig controls test settings in CI.
type CITestSkipAllow ¶ added in v0.1.45
type CITestSkipAllow struct {
// Package matches the Go import path. Accepted spellings: the full
// import path, the tail fragment people actually think in
// ("internal/threads"), or either with a "/..." subtree suffix.
Package string `yaml:"package"`
// Reason is why this package legitimately skips. Required.
Reason string `yaml:"reason"`
// MaxSkipRatio is the share THIS package may skip. Absent means fully
// exempt, which is the common case (an integration-only package on a
// machine with no docker skips 100%, correctly, forever).
MaxSkipRatio float64 `yaml:"max_skip_ratio,omitempty"`
}
CITestSkipAllow is one declared exemption from the mass-skip gate.
Reason is required and is not read by any logic: it exists to be read by a human in a code review. An exemption nobody had to justify is one nobody will revisit, which is how a suppression outlives the condition that earned it — the same contract as `forge project disown <path> --reason`.
type CITestSkipsConfig ¶ added in v0.1.45
type CITestSkipsConfig struct {
// MaxSkipRatio is the share of a package's tests that may skip before
// it is reported. Default 0.5 — "more than half of this package did
// not run". Range (0,1].
MaxSkipRatio float64 `yaml:"max_skip_ratio,omitempty"`
// MinTests is the sample-size floor below which a ratio is arithmetic
// rather than evidence (1-of-2 is 50% and means nothing). Default 5.
// It does not apply to a package that skipped EVERY test — "none of
// them ran" is unambiguous at any size.
MinTests int `yaml:"min_tests,omitempty"`
// Allow declares the packages whose heavy skipping is expected, so
// legitimate skips stop being noise. Each entry REQUIRES a reason.
Allow []CITestSkipAllow `yaml:"allow,omitempty"`
}
CITestSkipsConfig tunes the silent-mass-skip gate.
A Go suite that skips almost everything exits 0 and reads as green. Measured on one real package: 9 pass / 124 skip without DATABASE_URL, 103 pass / 1 skip with it — same exit code, 7% of the tests. `forge ci verify-test-run` reads the run's own `go test -json` output and reports the packages whose pass therefore means nothing.
Both numeric knobs default when absent (0 is not "disable it"); to switch the gate off, take the command out of the pipeline rather than setting a ratio of 1 and leaving a check that can never fire.
type CIVulnConfig ¶
type CIVulnConfig struct {
Go bool `yaml:"go"` // govulncheck, default true
Docker bool `yaml:"docker"` // trivy, default true
NPM bool `yaml:"npm"` // npm audit, default true
// Exemptions accepts specific advisory IDs that must not fail the
// gate. It exists for exactly one situation: a vulnerability with no
// fixed version, reached only through a code path the project does not
// execute. Without it the only options are a red gate forever or
// disabling the scan — and a disabled scan is how the NEXT advisory,
// the reachable one, goes unnoticed.
//
// One list serves both scanners, and each entry's ID says which one it
// belongs to: govulncheck reports GO-YYYY-NNNN ids, npm audit reports
// GHSA-xxxx-xxxx-xxxx ids. An id neither scanner reports fails the gate,
// since it can accept nothing.
//
// Scoped by ID on purpose: an exemption suppresses the advisories it
// names and nothing else, so a new CVE in the very same module or
// package still fails. See CIVulnExemption for the per-entry contract.
Exemptions []CIVulnExemption `yaml:"exemptions,omitempty"`
}
CIVulnConfig controls vulnerability scanning in CI.
func (CIVulnConfig) UsesDefaultScanners ¶ added in v0.1.45
func (c CIVulnConfig) UsesDefaultScanners() bool
UsesDefaultScanners reports whether the project left the scanner selection unset, which by project convention means "all enabled".
It deliberately ignores Exemptions: listing an accepted advisory says nothing about WHICH scanners should run, and letting it count as a selection would silently turn every unlisted scanner OFF as a side effect of accepting one CVE. This is why the config is no longer compared with `== CIVulnConfig{}` — adding the slice both broke that comparison at compile time and made it the wrong question.
type CIVulnExemption ¶ added in v0.1.45
type CIVulnExemption struct {
// ID is the advisory ID exactly as the scanner printed it: a Go
// advisory ID for govulncheck ("GO-2026-4887"), a GitHub advisory ID
// for npm audit ("GHSA-vfj7-8cjw-p6xm"). The namespace routes the
// entry to its scanner.
ID string `yaml:"id"`
// Reason states why this advisory cannot affect this project —
// the unreachability argument, not a ticket number.
Reason string `yaml:"reason"`
// Expires is the YYYY-MM-DD date this acceptance stops being
// honored, forcing a re-review.
Expires string `yaml:"expires"`
}
CIVulnExemption accepts one advisory ID against the vulnerability gate (govulncheck or npm audit), with the justification and the expiry date attached to it.
Both extra fields are REQUIRED, and that is the whole design. An allowlist of bare IDs rots invisibly: nobody can tell a still-valid acceptance from one whose premise disappeared two upgrades ago, so the safe-looking action is always to leave it. Reason makes the claim reviewable, and Expires forces the re-review to happen on a date rather than never — an exemption past its expiry FAILS the gate exactly as if it were not listed.
An entry that no longer matches anything is reported too. That is the good outcome (the advisory got a fix, or the dependency went away) and it should be deleted, not left behind to suppress a future finding nobody chose to accept.
type CRDConfig ¶
type CRDConfig struct {
// Name is the PascalCase CRD type name. e.g. "Workspace".
Name string
// Group is the API group, defaulting to the parent operator's
// Group. Carried explicitly so a single operator can manage CRDs
// from multiple groups.
Group string
// Version is the API version. Defaults to the parent operator's
// Version.
Version string
// Shape is the reconciler scaffold style. One of
// "state-machine" (phase-driven), "config" (declarative-only,
// no state), "composite" (manages sub-resources). Drives which
// template is rendered for the controller shim.
Shape string
}
CRDConfig represents a single Custom Resource Definition reconciled by an operator, discovered from the <crd-name>_controller.go shim that `forge scaffold crd <name> --operator <op>` writes. Untagged for the same reason as ComponentConfig: it is never serialized.
type ComponentConfig ¶
type ComponentConfig struct {
Name string
// Kind is THE discriminator: server|worker|cron|operator|binary.
// See the ComponentKind* constants.
Kind string
Path string
Schedule string // cron expression for kind=cron
ProtoPackages []string
// Group is the API group for kind=operator components. e.g.
// "reliant.dev". Read from the operator package's APIGroup const.
Group string
// Version is the API version for kind=operator components. e.g.
// "v1alpha1". Read from the operator package's APIVersion const.
Version string
// CRDs lists the CRDs reconciled by this operator — one per
// operators/<operator>/<crd-name>_controller.go on disk.
CRDs []CRDConfig
}
ComponentConfig describes one buildable/runnable unit of a forge project — a Connect server, worker, cron, operator or secondary binary. Its Kind field selects which scaffold + deploy treatment the component receives; see the ComponentKind* constants.
It carries NO struct tags, and that is load-bearing: a component is never serialized to or parsed from a config file. The declaration is the code (the proto descriptor, internal/workers/, internal/operators/, cmd/) and this type is only the in-memory shape codegen.DiscoverProjectComponents hands back after reading it. Per-environment facts (placement, replicas, exposed ports) are declared in `deploy/kcl/<env>/main.k`, not here.
func (ComponentConfig) EffectiveKind ¶
func (c ComponentConfig) EffectiveKind() string
EffectiveKind returns the lowercased, trimmed kind, defaulting to "server" for empty input (a component with no kind is a Connect server — the historical `type: go_service` default).
func (ComponentConfig) IsBinary ¶
func (c ComponentConfig) IsBinary() bool
IsBinary reports whether the component is a standalone binary subcommand.
func (ComponentConfig) IsCron ¶
func (c ComponentConfig) IsCron() bool
IsCron reports whether the component is a scheduled cron job.
func (ComponentConfig) IsJob ¶ added in v0.1.45
func (c ComponentConfig) IsJob() bool
IsJob reports whether the component is a one-shot job.
func (ComponentConfig) IsOperator ¶
func (c ComponentConfig) IsOperator() bool
IsOperator reports whether the component is a controller-runtime operator.
func (ComponentConfig) IsServer ¶
func (c ComponentConfig) IsServer() bool
IsServer reports whether the component is a Connect-RPC server.
func (ComponentConfig) IsWorker ¶
func (c ComponentConfig) IsWorker() bool
IsWorker reports whether the component is an in-process worker.
type ConfigGuardConfig ¶
type ConfigGuardConfig struct {
// EnforceTypedAccess selects the env-access guardrail strictness:
// "off" | "warn" | "error". Empty resolves to "warn" — use
// [ConfigGuardConfig.EffectiveEnforceTypedAccess], don't read the raw
// string. Unknown values are rejected at load time (validate.go).
EnforceTypedAccess string `yaml:"enforce_typed_access,omitempty"`
// LoaderPackage is the allowlisted package path that may read the
// environment directly — the home of forge's generated config loader.
// Empty resolves to [DefaultLoaderPackage] ("pkg/config"); use
// [ConfigGuardConfig.EffectiveLoaderPackage].
LoaderPackage string `yaml:"loader_package,omitempty"`
}
ConfigGuardConfig is the `config:` section of forge.yaml. It steers humans and LLMs away from reading the environment directly and toward forge's generated, dependency-injected typed config object.
The block is OPTIONAL. When absent, EnforceTypedAccess resolves to "warn" (see EnforceTypedAccessWarn) so existing projects gain the advisory guardrail without a flag-day; `forge project new` writes an explicit `enforce_typed_access: error` for greenfield projects, and an explicit `warn` when `--in-place` adopts an existing project.
ADOPTION / re-render: the strictness is projected into the generated .golangci.yml (forbidigo in `linters.enable` for error, settings-only for warn, absent for off). That file is a SCAFFOLD-ONCE, user-owned Tier-2 artifact — `forge generate` never re-renders it, and `forge project upgrade` only auto-updates it when the on-disk copy is an unedited forge render (a verifying forge:hash marker). A freshly-scaffolded .golangci.yml carries no marker and is "user-owned from birth", so after changing enforce_typed_access in forge.yaml the user must explicitly re-render: `rm .golangci.yml && forge project upgrade` (re-scaffold), or `forge project upgrade --force` when they have no local .golangci.yml edits. This is the deliberate Tier-2 contract — forge will not silently stomp a hand-tuned linter config — not a bug. The forbidigo `msg`, the template's warn-mode comment, and `forge lint`'s advisory line all teach this path.
func (ConfigGuardConfig) EffectiveEnforceTypedAccess ¶
func (c ConfigGuardConfig) EffectiveEnforceTypedAccess() string
EffectiveEnforceTypedAccess returns the resolved guardrail strictness, defaulting an absent/empty value to "warn" (advisory). It normalizes case and the "warning" alias. Validation (validate.go) has already rejected any other non-empty value, so this never silently swallows a typo.
func (ConfigGuardConfig) EffectiveLoaderPackage ¶
func (c ConfigGuardConfig) EffectiveLoaderPackage() string
EffectiveLoaderPackage returns the allowlisted loader package path, defaulting an absent/empty value to DefaultLoaderPackage.
func (ConfigGuardConfig) TypedAccessGuardEnabled ¶
func (c ConfigGuardConfig) TypedAccessGuardEnabled() bool
TypedAccessGuardEnabled reports whether the env-access guardrail should be emitted at all (true unless the strictness is "off").
func (ConfigGuardConfig) TypedAccessGuardGates ¶
func (c ConfigGuardConfig) TypedAccessGuardGates() bool
TypedAccessGuardGates reports whether the guardrail FAILS the build (true only in "error" mode; "warn" is advisory, "off" emits nothing).
type ConstraintKind ¶ added in v0.1.45
type ConstraintKind string
ConstraintKind classifies a named constraint by the kind of violation it produces, because that is the only thing a service branching on it cares about: a UNIQUE is a lost race, a CHECK is bad input, a FOREIGN KEY is a missing parent. Each maps to a different sentinel.
const ( ConstraintKindUnique ConstraintKind = "unique" ConstraintKindCheck ConstraintKind = "check" ConstraintKindForeignKey ConstraintKind = "foreign_key" )
The ConstraintKind values forge projects from the applied schema. There is deliberately no primary-key kind — see PlanEntity.Constraints.
type ContractsConfig ¶
type ContractsConfig struct {
Exclude []string `yaml:"exclude"` // packages that opt out
// InterfaceTypes lists additional cross-package interface types (over
// and above the built-in list in internal/generator/contract) that the
// mock generator should treat as mockable — i.e. emit "nil" as the
// fallback zero value instead of the invalid composite literal "T{}".
//
// Entries are matched against the rendered Go type expression of a
// contract method's return value, e.g. "billing.MeterClient" or
// "myproject.SomeProjectLocalInterface". Use this when a contract
// method returns a project-local interface that the mock generator
// would otherwise mistakenly treat as a struct.
InterfaceTypes []string `yaml:"interface_types"`
}
ContractsConfig controls contract enforcement linter behavior.
It carries only the two ESCAPE HATCHES the analyzer reads: which packages opt out (`exclude`) and which project-local types the mock generator must treat as interfaces (`interface_types`). The old severity dials (`strict`, `allow_exported_vars`, `allow_exported_funcs`) are gone: the contract rules are unconditional — a package either opts out by path or obeys them — and no analyzer ever read those three booleans. Whether the lint runs at all is `features.contracts`.
func (ContractsConfig) IsExcluded ¶
func (c ContractsConfig) IsExcluded(pkgPath string) bool
IsExcluded returns true if the given package path matches any exclude pattern. Delegates to MatchExclude — the shared matcher used by the contract analyzer and the forgeconv lint surface so all three places agree on what "excluded" means. See the doc on MatchExclude for the matching rules and the deliberate exit from the pre-2026-06 inline implementation (empty-pattern handling + slash-normalisation).
type DatabaseConfig ¶
type DatabaseConfig struct {
// Driver is "postgres" or "none" — derived, see DeriveDatabase.
Driver string `yaml:"-"`
// MigrationsDir is always db/migrations when there is a database.
MigrationsDir string `yaml:"-"`
MigrationSafety MigrationSafetyConfig `yaml:"migration_safety,omitempty"`
// ORM places the generated data layer. Absent, it is internal/db.
ORM ORMConfig `yaml:"orm,omitempty"`
// PostgresVersion is the postgres the project runs in production — a
// major ("16") or an exact release ("16.9.0") — and so the one its
// tests run against: forge/pkg/pgtest boots its embedded server at this
// version (and checks an external FORGE_TEST_POSTGRES_URL server's
// major against it). Empty means pgtest's default, the newest major.
// Read by pgtest itself from the nearest forge.yaml, so a bare
// `go test` honours it; FORGE_TEST_POSTGRES_VERSION overrides it for
// one run.
PostgresVersion string `yaml:"postgres_version,omitempty"`
}
DatabaseConfig holds database-related settings.
Only migration_safety is forge.yaml surface. The driver and migrations directory are DERIVED: a service project whose tree has db/migrations uses postgres and that directory, and one without it has no database (driver "none"). The dev seed has no config at all — rows, auto-seed and table scoping are fixed defaults (see internal/cli/db_seed.go).
type DeployConcurrency ¶
type DeployConcurrency struct {
Enabled bool `yaml:"enabled"` // default true
CancelInProgress bool `yaml:"cancel_in_progress,omitempty"` // default false
}
DeployConcurrency controls deployment concurrency settings.
type DeployConfig ¶
type DeployConfig struct {
Environments []DeployEnvConfig `yaml:"environments,omitempty"`
Concurrency DeployConcurrency `yaml:"concurrency,omitempty"`
FrontendDeploy string `yaml:"frontend_deploy,omitempty"` // "firebase", "vercel", "none"
MigrationTest bool `yaml:"migration_test,omitempty"` // test migrations before deploy
// TargetArch is the GOARCH the deploy target cluster runs on. When
// unset, forge defaults to amd64 (the predominant k8s host arch).
// Setting this at the project level means Mac/arm64 dev machines
// will cross-compile the Go binary (GOOS=linux GOARCH=<target>
// CGO_ENABLED=0) and pass --platform=linux/<target> to docker
// buildx so the image kubelet pulls actually runs on the node.
//
// Without cross-compile, an arm64-built image deployed onto an
// amd64 node fails at pod startup with the opaque kernel-level
// "exec format error". The CLI's --target-arch flag overrides
// this per-invocation.
TargetArch string `yaml:"target_arch,omitempty"`
}
DeployConfig holds deployment PIPELINE-CONTROL settings (target_arch, migration_test, concurrency, frontend_deploy) plus an input consumed by the CI-workflow generator (generator.CIWorkflows):
- the CI provider (github/gitlab/…) lives in `ci.provider`, not here — the dead `deploy.provider` field was removed (see removedSchemaKeys); generate_ci.go reads cfg.CI.Provider.
- there is no registry here: an image registry is declared in the env's KCL and nowhere else (see refusedSchemaKeys).
- `deploy.environments` supplies optional per-env auto/protection/url metadata for the generated deploy.yml. When empty, the deployable environment set is derived from the on-disk deploy/kcl/<env>/ directories (generator.CIWorkflows' promotion-ordered fallback).
func (*DeployConfig) EffectiveTargetArch ¶
func (d *DeployConfig) EffectiveTargetArch(override string) string
EffectiveTargetArch returns the deploy-target GOARCH. Order of precedence: explicit override (caller-provided), forge.yaml's deploy.target_arch, then the default "amd64". The "amd64" default reflects the empirical reality that the vast majority of k8s nodes are amd64; arm64 deployments must opt in via forge.yaml or --target-arch.
func (*DeployConfig) IsConcurrencyEnabled ¶
func (d *DeployConfig) IsConcurrencyEnabled() bool
IsConcurrencyEnabled returns true if deploy concurrency is enabled. Zero value is treated as enabled.
type DeployEnvConfig ¶
type DeployEnvConfig struct {
Name string `yaml:"name"` // staging, preprod, prod
Auto bool `yaml:"auto,omitempty"` // auto-deploy
Protection bool `yaml:"protection,omitempty"` // environment protection gates
URL string `yaml:"url,omitempty"` // environment URL
}
DeployEnvConfig defines a deployment environment.
type Deps ¶
type Deps struct{}
Deps is the dependency set for the config Service. Empty today; expanded when file-loading behavior moves into this package.
type DevStackConfig ¶ added in v0.1.45
type DevStackConfig struct {
// MaxStacks is how many dev stacks may hold a port block at once,
// counting the default stack (block 0). Zero means unset → the
// DefaultMaxStacks default, which is what every existing project gets.
//
// It bounds NEW allocations only. A registry that already holds a block
// at or past the ceiling keeps resolving it: a block multiplies into k3d
// host mappings and into the dev IdP's baked-in `iss` claim and redirect
// URIs, so lowering this value must never move a port that has already
// been issued. Reclaiming space is `forge env devstack prune`.
MaxStacks int `yaml:"max_stacks,omitempty"`
// BlockSize is the port-number distance between adjacent stacks —
// stack N's port for base B is B + N*BlockSize. Zero means unset → the
// DefaultBlockSize default (100), the value this was hardcoded to before
// it was configurable.
//
// It is exposed because it is half of the spacing invariant: two bases
// that are congruent modulo BlockSize collide at some block, and whether
// that block is reachable depends on MaxStacks. The
// forgeconv-allocate-port-spacing lint reads both.
BlockSize int `yaml:"block_size,omitempty"`
}
DevStackConfig bounds the parallel-dev-stack port allocator (fp.allocate_port(base, key) → base + block(key)*BlockSize).
Why this is CONFIG rather than a constant. The number of parallel stacks a project supports is not forge's to choose: it is fixed by the project's own cluster config. control-plane's deploy/k3d.yaml PRE-MAPS host ports at cluster-create time, so a stack whose block falls outside the pre-mapped range renders ports the cluster never mapped. Before this block existed the ceiling was stated twice — implicitly as "unbounded" in the allocator, and literally as a hand-maintained `block = 1..7` comment in k3d.yaml — with nothing tying them together. The 8th worktree therefore allocated cleanly and then failed as "gateway unreachable", nowhere near the cause.
Declaring it once makes the allocator refuse a block the cluster cannot host, and lets the pre-map range be generated from the same number.
func (DevStackConfig) EffectiveBlockSize ¶ added in v0.1.45
func (d DevStackConfig) EffectiveBlockSize() int
EffectiveBlockSize returns the configured port-block quantum, or the default when unset.
func (DevStackConfig) EffectiveMaxStacks ¶ added in v0.1.45
func (d DevStackConfig) EffectiveMaxStacks() int
EffectiveMaxStacks returns the configured parallel-stack ceiling, or the default when unset.
type DockerConfig ¶
type DockerConfig struct {
// BuildContexts maps a build-context name to anything `docker buildx
// --build-context name=value` accepts:
//
// - A local filesystem path. Relative paths are resolved against the
// project root (the directory holding forge.yaml). The typical
// case is a sibling-checkout local replace directive (e.g. a
// `replace x => ../x` in go.mod where ../x lives outside the
// project's build context).
// - A `docker-image://<image>` ref. Passed through verbatim so a
// Dockerfile `FROM <name>` can be overridden with a specific image
// at build time (local override of a base image during dev,
// pin-by-digest in CI, etc.).
// - A GitSource mapping (repo, ref, optional subdir). The CLI resolves
// it through the shared source cache and explicit local overrides.
// - Any other scheme buildkit understands (e.g. `oci-layout://`,
// `https://`). Anything containing `://` is passed through
// unchanged.
//
// Each entry becomes a `--build-context name=value` arg to `docker
// build`, letting Dockerfiles consume it via `FROM <name>` or
// `COPY --from=<name>`. Empty when not set; existing projects with no
// contexts see no change in build behaviour or output.
BuildContexts map[string]BuildContext `yaml:"build_contexts,omitempty"`
}
DockerConfig holds Docker build configuration for the PROJECT image (the root `Dockerfile` built by `forge build`). Per-service DockerBuild services declared in KCL carry their OWN build_contexts on the KCL DockerBuild block; these are the project-image defaults / fallback.
There is no registry here, and none on any env either: an image registry is part of a WORKLOAD's `image` in deploy/kcl/workloads.k. The project image is tagged and pushed to the reference the workload declaring it wrote, which is the same reference the runtime pulls.
forge is FULLY base-image-AGNOSTIC: it does NOT discover, mirror, pin, or inject base images, and offers no mirror/pull-through setting. A Dockerfile's `FROM` lines are the COMPLETE source of truth — pin with `FROM …@sha256:…`, and route through a pull-through mirror by writing the mirror host into the `FROM` ref directly (e.g. `FROM us-docker.pkg.dev/<p>/dockerhub/alpine:3.21`). forge never rewrites a FROM.
type FeatureName ¶
type FeatureName = string
FeatureName is the canonical feature key. Stays a string alias so the constants below are usable directly anywhere the feature name shows up, e.g. in the strings emitted by `forge project audit --json | jq '.features'`.
const ( FeatureORM FeatureName = "orm" FeatureCodegen FeatureName = "codegen" FeatureMigrations FeatureName = "migrations" FeatureCI FeatureName = "ci" FeatureBuild FeatureName = "build" FeatureContracts FeatureName = "contracts" FeatureFrontend FeatureName = "frontend" FeatureObservability FeatureName = "observability" FeatureHotReload FeatureName = "hot_reload" FeatureDeploy FeatureName = "deploy" FeatureIngress FeatureName = "ingress" FeatureOperators FeatureName = "operators" )
Feature name constants — the wire format of `forge project audit --json` and `forge project features`.
func FeatureNames ¶ added in v0.1.45
func FeatureNames() []FeatureName
FeatureNames returns every feature name in display order.
type FeaturesConfig ¶
type FeaturesConfig struct {
Codegen *bool `yaml:"codegen,omitempty"`
Migrations *bool `yaml:"migrations,omitempty"`
CI *bool `yaml:"ci,omitempty"`
Build *bool `yaml:"build,omitempty"`
Contracts *bool `yaml:"contracts,omitempty"`
Frontend *bool `yaml:"frontend,omitempty"`
Observability *bool `yaml:"observability,omitempty"`
HotReload *bool `yaml:"hot_reload,omitempty"`
Deploy *bool `yaml:"deploy,omitempty"`
Ingress *bool `yaml:"ingress,omitempty"`
Operators *bool `yaml:"operators,omitempty"`
// contains filtered or unexported fields
}
FeaturesConfig is the resolved on/off state of each forge subsystem (codegen, orm, deploy, ...).
Every feature DERIVES from what exists in the repository — whether db/migrations exists, whether a frontend exists, whether deploy/kcl declares a gateway, whether internal/operators exists, whether internal/db carries a `//forge:no-orm` marker. See DeriveFeatureDefaults for the rule per feature.
A derived default is still only a default. The `features:` block pins one on or off whatever the tree says:
features: observability: false # no generated dashboards, even with internal/handlers/ hot_reload: false
That is the switch a structural change needs. Creating internal/handlers/ makes a CLI-shaped project server-shaped, and observability follows the server's sources; a project that adopts forge's handler model without wanting forge's dashboards says so here, once, and no later change to the tree turns them back on. A pinned value is never normalized away on write, even when it matches what the tree derives today: matching today is not the same as meaning it tomorrow. A derived feature whose requirement is pinned off follows it off (ingress with deploy, orm with codegen); a pinned feature whose requirement is off is a load error.
The ORM is the one feature with no key: its switch is `//forge:no-orm` in internal/db's doc comment, beside the code it affects, and a second spelling here would be a second source of truth.
A FeaturesConfig not produced by the config loader (the zero value, or one hand-built in a test) resolves every feature to "enabled" except the default-off ones; With overrides a single feature on such a value.
func (FeaturesConfig) BuildEnabled ¶
func (f FeaturesConfig) BuildEnabled() bool
BuildEnabled reports whether `forge build` is enabled (default: on). Direct `forge build` invocations error when off; orchestrators like `forge env up` log a skip line and continue.
func (FeaturesConfig) CIEnabled ¶
func (f FeaturesConfig) CIEnabled() bool
CIEnabled reports whether the CI feature is on (default: on).
func (FeaturesConfig) CodegenEnabled ¶
func (f FeaturesConfig) CodegenEnabled() bool
CodegenEnabled reports whether codegen is on (default: on).
func (FeaturesConfig) ContractsEnabled ¶
func (f FeaturesConfig) ContractsEnabled() bool
ContractsEnabled reports whether contract enforcement is on (default: on).
func (FeaturesConfig) DeployEnabled ¶
func (f FeaturesConfig) DeployEnabled() bool
DeployEnabled reports whether the deploy feature is on. Stable flag: absent derives from the tree (deploy ⇔ deploy/kcl exists — see DeriveFeatureDefaults), explicit `features.deploy: true|false` wins. Service scaffolds ship a deploy/kcl tree, so deploy is ON for the canonical service shape; a tree without one derives OFF.
func (FeaturesConfig) EffectiveFeatures ¶
func (f FeaturesConfig) EffectiveFeatures() map[string]bool
EffectiveFeatures projects the resolved enabled/disabled state of every feature into a stable name→bool map, for `forge project audit` and tests.
func (FeaturesConfig) FollowingRequirements ¶ added in v1.0.0
func (f FeaturesConfig) FollowingRequirements() FeaturesConfig
FollowingRequirements returns f with the feature graph applied to its current state exactly as the loader applies it to a derived one (followPinnedRequirements): every unpinned feature whose required feature is off is turned off too, transitively. `forge project new` calls it on the feature set it is about to write a tree for, so the scaffold and every later load of that tree agree — one rule, in one place. Before, the scaffold kept a feature on that the loader then derived off (`--disable codegen` still wrote db/migrations and a database-backed dev env), or carried a hand-copied cascade of the edges.
func (FeaturesConfig) FrontendEnabled ¶
func (f FeaturesConfig) FrontendEnabled() bool
FrontendEnabled reports whether the frontend feature is on (default: on).
func (FeaturesConfig) HotReloadEnabled ¶
func (f FeaturesConfig) HotReloadEnabled() bool
HotReloadEnabled reports whether the hot-reload feature is on (default: on).
func (FeaturesConfig) IngressEnabled ¶
func (f FeaturesConfig) IngressEnabled() bool
IngressEnabled reports whether Gateway API ingress is wired (`features.ingress`; derives to false). When off, forge skips ingress codegen, `forge cluster up` skips the Envoy Gateway + GatewayClass install, `forge cluster urls` returns nothing, and the audit ingress category is suppressed.
func (FeaturesConfig) MigrationsEnabled ¶
func (f FeaturesConfig) MigrationsEnabled() bool
MigrationsEnabled reports whether the migrations feature is on (default: on).
func (FeaturesConfig) ORMEnabled ¶
func (f FeaturesConfig) ORMEnabled() bool
ORMEnabled reports whether the ORM feature is on (default: on).
func (FeaturesConfig) ObservabilityEnabled ¶
func (f FeaturesConfig) ObservabilityEnabled() bool
ObservabilityEnabled reports whether the observability feature is on (default: on).
func (FeaturesConfig) OperatorsEnabled ¶
func (f FeaturesConfig) OperatorsEnabled() bool
OperatorsEnabled reports whether controller-runtime operator codegen + CRD manifest generation is wired (`features.operators`; derives to false). When off, the operator binary codegen + CRD scaffold steps skip silently and `forge scaffold operator` errors.
func (FeaturesConfig) Pin ¶ added in v0.1.45
func (f FeaturesConfig) Pin(name FeatureName, on bool) (_ FeaturesConfig, ok bool)
Pin returns a copy with name pinned on or off, as a `features:` entry in forge.yaml would. ok is false for a feature with no key (orm, whose switch is //forge:no-orm) or an unknown name; the copy is then unchanged.
func (FeaturesConfig) Pinned ¶ added in v0.1.45
func (f FeaturesConfig) Pinned(name FeatureName) bool
Pinned reports whether forge.yaml's `features:` block sets name, rather than the tree deriving it.
func (FeaturesConfig) With ¶ added in v0.1.45
func (f FeaturesConfig) With(name FeatureName, on bool) FeaturesConfig
With returns a copy with one feature forced on or off. The scaffold uses it to describe a project that does not exist on disk yet (a CLI or library has no codegen); nothing persists it, because the next load derives the same answer from the tree the scaffold wrote.
type FrontendConfig ¶
type FrontendConfig struct {
Name string
// Type is "nextjs", "react-native" or "vite-spa" — detected from the
// framework's own config file, or the KCL declaration's `type`.
Type string
// Source pins this frontend's code to another repository at a ref (the
// KCL `source = forge.GitSource {...}`). nil for an in-repo frontend.
Source *GitSource
// Port is the dev-server listen port (KCL `port`). 0 means EPHEMERAL:
// `forge env up` allocates a free OS port at launch.
Port int
// DevRunner is the package manager forge drives the frontend with:
// "npm", "pnpm" or "yarn". KCL `dev_runner`, else the lockfile on disk.
DevRunner string
// BasePath is the URL prefix the frontend is mounted under ("/admin"),
// "" for the host root. KCL `base_path`, else the literal fallback in the
// frontend's own next.config.ts. Only meaningful for nextjs.
BasePath string
// Output is the Next.js build shape: "standalone", "static" or "server".
// Read from the frontend's own next.config.ts; "" means standalone.
Output string
// Routes restricts which entities this frontend gets generated CRUD pages
// for (KCL `routes`): route SLUGS ("llm-keys"), an ALLOWLIST. Empty means
// every CRUD entity. The single value `none` (RoutesNone) means NO
// generated pages at all. An adopted frontend that declares none gets
// `none` (frontend_codegen.go).
Routes []string
// Adopted reports that forge did not scaffold this frontend — it existed
// before forge generated into it. generate writes only Tier-1 codegen and
// the owned seams that code imports into an adopted frontend: no starter
// components, no CRUD pages, nav or dashboard scaffolds, and no edits to
// its package.json. Read off disk (frontendScaffoldedByForge); false for
// a FrontendConfig built in code, which is a frontend forge made.
Adopted bool
// Hooks is the package.json `forge.hooks` allowlist: the proto services
// that get generated React Query hooks in this frontend. nil means every
// service; an empty list means none. See WantsHooksFor.
Hooks []string
// ComponentsDir is the package.json `forge.components_dir`: where forge's
// component library lives, relative to the frontend directory. "" means
// DefaultComponentsDir. Read it through ComponentsDirRel.
ComponentsDir string
// CodegenProblems are the reasons the frontend's package.json forge block
// could not be honoured (an unknown key, an escaping components_dir).
// generate refuses to run while any is present.
CodegenProblems []string
// contains filtered or unexported fields
}
FrontendConfig is one frontend in a project's inventory.
There is no `frontends:` key in forge.yaml. Every field is either declared in the KCL `forge.Frontend` (kcl/schema.k) or read off the frontend's own files (frontend_inventory.go), so nothing here is a setting you write.
func DeriveFrontendsFromKCL ¶ added in v0.1.45
func DeriveFrontendsFromKCL(projectDir string, kcl []KCLFrontend) []FrontendConfig
DeriveFrontendsFromKCL folds the KCL-declared frontends this project owns the code for into inventory entries, deduplicated by name and ordered by name so two renders of the same project produce the same inventory — several environments normally declare the same frontend, and map iteration would otherwise make generate's output depend on which env was read first.
func DiscoverInRepoFrontends ¶ added in v0.1.45
func DiscoverInRepoFrontends(projectDir string) []FrontendConfig
DiscoverInRepoFrontends finds the frontends sitting in this project's frontends/ directory, identified by the framework config file each kind's scaffold ships. It is the render-INDEPENDENT half of the fallback.
The KCL is the better source when it is available — it is the file the project maintains, and it carries the declaration verbatim. But a KCL module that does not compile yields nothing, and "no frontends" is indistinguishable in its effects from the bug this whole file exists to fix: emitters walk an empty list, thirty already-generated files are reported stale, and config_gen.ts silently stops tracking its proto.
Making a project's codegen depend on its deploy graph COMPILING is the wrong coupling. A developer mid-edit in deploy/kcl — or one whose KCL broke for an unrelated reason — must still be able to regenerate the frontend that is plainly there on disk. So this runs when the render produced nothing, and the two sources agree by construction on the case that matters: a directory under frontends/ is inside the project by definition, so the containment rule that excludes sibling repos cannot be violated here at all.
Only frontends/<name> is scanned. A frontend at a custom path is discoverable from KCL alone, which is the correct asymmetry: a non-conventional layout is exactly the case where a declaration should be required rather than guessed.
func DiscoverRootFrontend ¶ added in v1.0.0
func DiscoverRootFrontend(projectDir string) (FrontendConfig, bool)
DiscoverRootFrontend finds a static site's frontend: the app at the project root itself (`forge project new --in-place --kind site`), which the frontends/<name> scan cannot see.
Admitted only when the project has NO go.mod. A site is its frontend and its deploy envs, nothing else, so the root carrying the framework marker is the frontend; a Go project's root is its module, and a stray vite.config there is not a frontend this rule should guess at — such a project declares its frontend in KCL, which every rendering command reads.
It matters because only the generate pipeline renders KCL to learn the frontend inventory (stepDeriveFrontendInventory). Without this, a root frontend was generated for and then neither linted, typechecked nor built by `forge build`: the same divergence ResolveInventoryAtLoad exists to close, for the one layout the directory scan missed.
func MergeFrontendInventory ¶ added in v0.1.45
func MergeFrontendInventory(disk, kcl []FrontendConfig) []FrontendConfig
MergeFrontendInventory overlays the KCL-declared frontends onto the ones discovered on disk, by name.
KCL is authoritative for every field it declares — path, type, port, dev_runner, base_path, routes — because it is the file the project maintains as the statement of what its frontends ARE. The disk supplies the rest: the Next.js output shape, and any of type/dev_runner/base_path the declaration left out (the scaffolded stanza omits `type`, so a Vite app would otherwise default to nextjs). A KCL frontend with no directory on disk was already excluded by DeriveFrontendsFromKCL (it is not this repo's to generate into), and a directory with no KCL declaration stays as discovered.
The result is sorted by name so two runs of the same project produce the same inventory.
func (FrontendConfig) ComponentsDirRel ¶ added in v1.0.0
func (f FrontendConfig) ComponentsDirRel() string
ComponentsDirRel is where forge's component library lives in this frontend, relative to its directory: the package.json components_dir, or DefaultComponentsDir.
func (FrontendConfig) DeclaredDir ¶ added in v0.1.45
func (f FrontendConfig) DeclaredDir() string
DeclaredDir is the path exactly as declared or resolved, with no fallback and no containment check — empty when forge.yaml named none.
Use it ONLY where the empty case, or an out-of-tree path, is the thing being asked about: reporting a sibling-repo frontend, deciding whether a declaration exists, or handing a path to a resolver that will rewrite it. Every caller that wants "the directory to read or write files in" wants Dir instead, which is why this one is deliberately awkward to use by accident.
func (FrontendConfig) Dir ¶ added in v0.1.45
func (f FrontendConfig) Dir(projectDir string) (string, bool)
Dir resolves the frontend's directory against the project root and reports whether it is inside it. It is the ONE answer to "where does this frontend live", and it subsumes both helpers it replaced: EffectivePath's fallback (empty `path:` means frontends/<name>) and FrontendDirWithin's containment check, which the fallback alone never had.
ok == false means there is no directory in THIS repository to speak of, for one of two reasons the caller almost always wants to treat alike:
- the path escapes the project root (`../reliant/web`) — it names another repository's working tree, whose own forge owns its codegen. Writing here puts one repo's generated TypeScript in another repo's git status.
- the frontend is unnamed, so the convention has nothing to fill in.
A `source:` frontend resolves normally ONCE its pin has been materialized, because resolution rewrites path to the cache directory; before that it has no in-tree location and reports ok == false rather than the invented frontends/<name>. That is the behavior difference from the retired EffectivePath, which returned the conventional label for a tree that was not there — fine for a caller printing a name, wrong for the many that then shelled into it.
The returned path is slash-separated and relative to projectDir.
func (FrontendConfig) EffectiveDevRunner ¶ added in v0.1.45
func (f FrontendConfig) EffectiveDevRunner() string
EffectiveDevRunner returns the frontend's package manager, defaulting to npm.
func (FrontendConfig) EffectiveOutput ¶ added in v0.1.45
func (f FrontendConfig) EffectiveOutput() string
EffectiveOutput returns the frontend's Next.js production shape, lowercased. An unset field is "standalone": the shape every frontend scaffolded before the field was always written was given, and still has in its scaffold-once next.config.ts. See the Output field.
func (FrontendConfig) HasGitSource ¶ added in v0.1.45
func (f FrontendConfig) HasGitSource() bool
HasGitSource reports whether this frontend's code comes from another repository rather than from a directory in the project tree.
func (FrontendConfig) IsNextJS ¶ added in v0.1.45
func (f FrontendConfig) IsNextJS() bool
IsNextJS reports whether the frontend is a Next.js app. An empty type is Next.js: it is the scaffold default, and the load seam fills it in for a frontend whose directory holds a next.config.
func (FrontendConfig) RoutesNone ¶ added in v0.1.45
func (f FrontendConfig) RoutesNone() bool
RoutesNone reports whether this frontend opted out of generated CRUD pages entirely (`routes: [none]`).
func (FrontendConfig) StaticExport ¶ added in v0.1.45
func (f FrontendConfig) StaticExport() bool
StaticExport reports whether the frontend's production build is a static export — files a CDN or bucket can serve with no server behind them. That is the precondition of every static runtime (forge.OnHosted for a frontend, forge.OnBucket, forge.OnFirebase): each publishes the build's public_dir and runs nothing.
Only a Next.js frontend has a choice. A Vite SPA and an Expo web build emit static files by construction, whatever `output:` says.
func (FrontendConfig) UnknownHooks ¶ added in v1.0.0
func (f FrontendConfig) UnknownHooks(known map[string]bool) []string
UnknownHooks returns the allowlist entries that name none of known (proto service names, either short or fully qualified), sorted.
func (FrontendConfig) WantsHooksFor ¶ added in v1.0.0
func (f FrontendConfig) WantsHooksFor(name, fullName string) bool
WantsHooksFor reports whether this frontend gets generated React Query hooks for the named service. nil Hooks (no allowlist) means every service; an entry matches the proto service name ("ChatService") or its fully-qualified name ("reliant.v1.ChatService").
func (FrontendConfig) WithDir ¶ added in v0.1.45
func (f FrontendConfig) WithDir(dir string) FrontendConfig
WithDir returns a copy whose directory is dir, and whose code is therefore IN this tree — so it clears Source.
Clearing it is the point, not a side effect. Source means "the code is in another repository and there is no directory here yet", which is exactly what Dir reports ok == false for. A resolver that materialized the pin has made that false: the code now sits in the cache directory it is passing in. Leaving Source set would keep every reader excluding a frontend whose code is on disk and ready to build — the shape of the original `forge build --target reliant-web` bug.
It is also how a caller assembles a FrontendConfig from a declaration that did not come from forge.yaml (a KCL entity, a discovered directory), which is why the parameter is a directory rather than a pin.
type FrontendLintConfig ¶
type FrontendLintConfig struct {
CSSHealth bool `yaml:"css_health,omitempty"` // enable stylelint-backed CSS health checks
NoImportant string `yaml:"no_important,omitempty"` // error, warn, off
NoInlineStyles string `yaml:"no_inline_styles,omitempty"` // error, warn, off
// Typecheck is the severity of the TypeScript typecheck lane
// ("error" | "warn" | "off"); see [FrontendLintConfig.EffectiveTypecheck].
Typecheck string `yaml:"typecheck,omitempty"`
}
FrontendLintConfig configures the frontend slice of `forge lint`: whether the stylelint-backed CSS health checks run, and which severity ("error"/"warn"/"off") the `no-important`, `no-inline-styles`, and `typecheck` rules use.
func (FrontendLintConfig) EffectiveNoImportant ¶
func (c FrontendLintConfig) EffectiveNoImportant() string
EffectiveNoImportant returns the configured severity for the no-important rule, falling back to "warn" when unset/invalid.
func (FrontendLintConfig) EffectiveNoInlineStyles ¶
func (c FrontendLintConfig) EffectiveNoInlineStyles() string
EffectiveNoInlineStyles returns the configured severity for the no-inline-styles rule, falling back to "warn" when unset/invalid.
func (FrontendLintConfig) EffectiveTypecheck ¶ added in v0.1.45
func (c FrontendLintConfig) EffectiveTypecheck() string
EffectiveTypecheck returns the configured severity for the frontend TypeScript typecheck lane, defaulting to "error" when unset or invalid.
The default gates on purpose: a type error is a real defect that the Go half of the pipeline can never see, and shipping it is exactly the class of break this lane exists to catch. Projects that want the signal without the gate set "warn"; "off" removes the lane entirely.
Note this dial governs TYPE ERRORS only. A typecheck that could not RUN (deps not installed, no TypeScript resolvable) is always reported as a warning — never silently as a pass — and escalates to an error under `forge lint --strict`.
type GenerateConfig ¶ added in v0.1.45
type GenerateConfig struct {
// Hooks are project-declared commands run in declaration order. See
// [GenerateHook].
Hooks []GenerateHook `yaml:"hooks,omitempty"`
}
GenerateConfig is the `generate:` block: what `forge generate` runs on the project's behalf beyond its own code generation.
type GenerateHook ¶ added in v0.1.45
type GenerateHook struct {
// Name labels the hook in generate's output and in errors. Required and
// unique.
Name string `yaml:"name"`
// Command is the argv to execute: no shell, so no quoting rules differ
// between platforms. Spell a shell pipeline as [sh, -c, "..."]. A
// relative command[0] with a path separator resolves against Dir.
Command []string `yaml:"command"`
// Dir is the project-relative working directory. Empty is the project
// root.
Dir string `yaml:"dir,omitempty"`
// Writes are the project-relative files or directories the command may
// write. They are captured into generate's rollback journal before the
// hook runs, so a failed generate restores them like forge's own output.
// An undeclared write is not restored; the post-rollback verification
// names it instead of claiming the tree is back to its pre-run state.
Writes []string `yaml:"writes,omitempty"`
}
GenerateHook is one command `forge generate` runs, after every step that writes forge's own output (proto stubs, ORM, mocks, composition, the goimports pass, the stale-codegen sweep) — so a hook that compiles the tree sees this run's output in its final form — and before the `go mod tidy` and `go build` that validate the result. Code a hook emits is therefore tidied and compiled in the same run that produced it.
It is the escape hatch for a generator forge does not own: a query generator kept while a project ports onto the ORM (sqlc), a project's own codegen tool, a second buf template, a schema or docs dump. forge does not interpret the command; it runs it, streams its output (and keeps it in .forge/generate-hooks/<name>.log), and fails the generate if it exits non-zero, repeating the tail of that output in the failure report. The command runs with the codegen tools go.mod pins (buf, protoc-gen-go, protoc-gen-connect-go) first on its PATH, the same ones forge's own buf step runs.
generate:
hooks:
- name: sqlc
command: [sqlc, generate]
writes: [internal/db/postgres/generated]
type GitSource ¶ added in v0.1.45
type GitSource struct {
// Repo is the repository: host/owner/name
// ("github.com/reliant-labs/reliant"), which forge expands to an https
// clone URL, or an explicit https://, ssh://, or git@host:owner/name
// URL, which is used verbatim.
Repo string `yaml:"repo" json:"repo"`
// Ref is the tag, branch, or commit sha to check out. Required —
// forge does not default to a repository's default branch, because an
// unpinned dependency is precisely the problem this type solves.
Ref string `yaml:"ref" json:"ref"`
// Subdir is the path within the repository holding this component's
// source ("web"). Empty means the repository root.
Subdir string `yaml:"subdir,omitempty" json:"subdir,omitempty"`
}
GitSource declares a component's code as "that repository at that ref" rather than a directory that must already exist on disk.
It is the schema half of internal/gitsource (which holds the resolver, the cache and the local-override rules); this type is the plain data forge.yaml and KCL both unmarshal into. It lives here rather than in gitsource so internal/config keeps its no-internal-imports shape — the resolver converts to its own Source type at the boundary.
The point is reproducibility, not convenience. A filesystem path to a sibling checkout fails wherever the sibling is absent (CI), and where it IS present it silently ships whatever happened to be checked out, so two machines produce different artifacts from identical commits. A ref makes the cross-repo dependency an explicit, reviewable, promotable version — the same property go.mod already gives the Go half of the same dependency.
type K8sConfig ¶
type K8sConfig struct {
KCLDir string `yaml:"kcl_dir"`
}
K8sConfig holds Kubernetes configuration.
type KCLFrontend ¶ added in v0.1.45
type KCLFrontend struct {
Name string
Type string
// Path is the frontend's directory as the KCL declared it, relative
// to the project root (or absolute). Empty means the KCL named no
// path, in which case the frontends/<name> convention applies.
Path string
// HasSource reports whether the declaration pins its code to another
// repository. Such a frontend has no directory in this tree by
// design, so it is never a codegen target here.
HasSource bool
// Port, DevRunner and BasePath are the declaration's dev-server port,
// package manager and mount prefix; zero/empty means it declared none.
Port int
DevRunner string
BasePath string
// Routes is the CRUD-page allowlist (forge.Frontend.routes); empty means
// every CRUD entity, ["none"] means no generated pages.
Routes []string
}
KCLFrontend is the narrow view of a KCL-declared frontend this package needs to fold one into the inventory. internal/config is a pure schema package with no internal imports, so callers (internal/cli's FrontendEntity, internal/doctor's renderedFrontend) convert into it at their own boundary rather than making this package depend on theirs.
func (KCLFrontend) InRepoDir ¶ added in v0.1.45
func (k KCLFrontend) InRepoDir(projectDir string) (string, bool)
InRepoDir resolves the declaration to a path inside this project, and reports whether it is one at all. A cross-repo `source:` pin never is: its code is materialized into a machine-local cache at build time, so there is no in-tree directory to speak of.
func (KCLFrontend) MissingInRepoCode ¶ added in v0.1.45
func (k KCLFrontend) MissingInRepoCode(projectDir string) bool
MissingInRepoCode reports the complement of OwnsFrontendCode within the same admission test: this project owns the frontend's LOCATION — a contained path, no cross-repo pin — and there is no directory there.
That is the one frontend forge genuinely cannot build. Every other shape has code somewhere forge can reach: a pinned source is materialized from the pin at build time, and a path pointing out of the tree names a checkout whose presence is a property of the machine rather than of this repository's configuration.
Restricting it to contained paths is what keeps the answer stable on a bare CI checkout, which is the condition every deployability check is required to be answerable under. A sibling-path frontend would otherwise be reported on every CI run and pass on every developer's machine — a verdict that flips with the working copy trains the reader to ignore the whole report.
func (KCLFrontend) OwnsFrontendCode ¶ added in v0.1.45
func (k KCLFrontend) OwnsFrontendCode(projectDir string) bool
OwnsFrontendCode reports whether THIS project's tree contains the frontend's source, which is precisely the condition under which forge may generate into it.
The test is containment, not existence-of-a-directory: a path that escapes the project root (`../reliant/web`) belongs to another repository whose owner generates its own code. Writing this project's hooks there would put one repo's generated TypeScript in another repo's working tree, where its own `forge generate` does not know to refresh it and its own git status reports it as an unexplained edit.
control-plane states the same rule from the other side: its reliant-web declaration in deploy/kcl/lib/services.k carries the comment "control-plane codegen must never be projected into the sibling working tree". That invariant was previously enforced only by a human remembering to leave a name out of a list; here it is a property of the path.
A cross-repo `source:` pin is excluded for the same reason and one more: its code is materialized into a machine-local cache at build time, so there is no in-tree directory to generate into at all.
type LintConfig ¶
type LintConfig struct {
Frontend FrontendLintConfig `yaml:"frontend,omitempty"`
// HandlerFileMaxLOC is the per-file LOC threshold above which the
// forgeconv-handler-file-size analyzer warns. Counts non-blank, non-
// comment Go source lines under handlers/<svc>/*.go. A value of 0 (or
// the field unset) is treated as the built-in default — see
// [LintConfig.EffectiveHandlerFileMaxLOC] for the canonical value.
HandlerFileMaxLOC int `yaml:"handler_file_max_loc,omitempty"`
// Rules is the repo-wide severity dial for forge-owned lint rules:
// rule ID → "off" | "warn" | "error". It is the COARSEST of the
// three suppression levels (see internal/linter/suppress) and the
// one to reach for last, because — like a path glob in
// .golangci.yml — it silently swallows every future finding for
// that rule, including the one you would have wanted to see.
//
// Prefer a file or line directive when the exemption is local. This
// map is for rules that genuinely do not apply to the whole project
// (a CLI has no handlers; an operator has no frontend), where the
// alternative is annotating every file.
//
// The `warn` setting is the load-bearing one: it lets a project
// adopt a newly-added opinionated rule incrementally instead of
// choosing between "fails the build today" and "off".
//
// Only forge's OWN rules are addressable here. golangci-lint,
// eslint, stylelint and buf each have their own config file and
// their own suppression syntax, and forge deliberately does not
// proxy them — a second place to configure the same linter is a
// place for the two to disagree.
//
// The special key "*" applies to every forge rule; an exact rule ID
// beats it.
Rules map[string]string `yaml:"rules,omitempty"`
// Tracing configures the tracing lane (`forge lint --tracing`).
Tracing TracingLintConfig `yaml:"tracing,omitempty"`
// AllowSkip accepts, by name and with a reason, a gating lane that
// could NOT run. Such a lane fails `forge lint` by default — a check
// that did not run is not a check that passed — so this is the
// project-level opt-out, the forge.yaml twin of `forge lint
// --allow-skip`. An accepted lane is still listed as SKIPPED (allowed)
// in the verdict, with its reason. It accepts only "could not run": a
// lane that ran and found problems still fails.
AllowSkip []LintSkipAllowance `yaml:"allow_skip,omitempty"`
}
LintConfig holds lint-related settings.
There is intentionally NO `contract:` toggle here. Whether the contract lint runs is `features.contracts` — the flag the gate actually resolves; a second `lint.contract` boolean had no reader at all.
func (LintConfig) EffectiveHandlerFileMaxLOC ¶
func (c LintConfig) EffectiveHandlerFileMaxLOC() int
EffectiveHandlerFileMaxLOC returns the LOC threshold for the handler-file-size analyzer, defaulting to DefaultHandlerFileMaxLOC when the config value is zero or unset.
func (LintConfig) EffectiveRules ¶ added in v0.1.45
func (c LintConfig) EffectiveRules() suppress.RuleSeverities
EffectiveRules returns the rule-severity map in the form the linters consume, with keys lowercased so forge.yaml is not case-sensitive about rule IDs.
func (LintConfig) ValidateRules ¶ added in v0.1.45
func (c LintConfig) ValidateRules() []string
ValidateRules reports rule-severity entries whose SEVERITY forge cannot interpret ("loud" rather than off / warn / error).
`forge lint` reports each as an error (the lint.rules check, internal/cli/lint/lint_rules.go), together with every KEY it cannot honour — a rule ID no lane emits, another tool's rule, a lane's could-not-run headline. The key half lives with the lanes, because only they know their rule IDs; it can be strict because `forge lint` runs under the project's pinned forge, so the key set is exactly the one that forge grades.
type LintSkipAllowance ¶ added in v1.0.0
type LintSkipAllowance struct {
// Lane is the lane's name as the verdict prints it ("frontend lint")
// or its flag spelling ("frontend-lint").
Lane string `yaml:"lane"`
// Reason says why the lane may not run here. Required.
Reason string `yaml:"reason"`
}
LintSkipAllowance is one lint.allow_skip entry. Read by the lint command (internal/cli/lint/lint_skip.go), which refuses an entry with an unknown lane or an empty reason.
type LogWrapperConfig ¶ added in v1.0.0
type LogWrapperConfig struct {
// Call is the helper, as "<import path>.<Func>":
// github.com/acme/app/internal/logging.Info.
Call string `yaml:"call"`
// ContextCall is what a call is rewritten to, as "<import path>.<Func>",
// taking the context first and then the helper's arguments unchanged
// (log/slog.InfoContext). Empty means "<Call>Context" in the helper's
// own package.
ContextCall string `yaml:"context_call,omitempty"`
}
LogWrapperConfig is one context-free logging helper and its context-taking replacement.
type MigrationSafetyConfig ¶
type MigrationSafetyConfig struct {
Enabled *bool `yaml:"enabled,omitempty"` // nil = enabled
UnsafeAddColumn string `yaml:"unsafe_add_column,omitempty"` // error, warn, off
DestructiveChange string `yaml:"destructive_change,omitempty"` // error, warn, off
VolatileDefault string `yaml:"volatile_default,omitempty"` // warn, error, off
// RequireCompatDeclaration makes `forge lint --migration-safety` require
// every migration after the baseline to declare `-- forge:backward-compatible
// — <why>` or `-- forge:breaks-rollback <why>` (migration-compat-undeclared,
// migration-compat-reason). Off by default.
RequireCompatDeclaration bool `yaml:"require_compat_declaration,omitempty"`
}
MigrationSafetyConfig controls migrationlint's three severity dials (unsafe add-column, destructive change, volatile default). An intentional destructive migration is marked in the file itself with `-- forge:allow-destructive`; there is no forge.yaml allowlist.
func (MigrationSafetyConfig) EffectiveDestructiveChange ¶
func (c MigrationSafetyConfig) EffectiveDestructiveChange() string
EffectiveDestructiveChange returns the configured severity for the destructive-change rule, falling back to "error" when unset/invalid.
func (MigrationSafetyConfig) EffectiveUnsafeAddColumn ¶
func (c MigrationSafetyConfig) EffectiveUnsafeAddColumn() string
EffectiveUnsafeAddColumn returns the configured severity for the unsafe-add-column rule, falling back to "error" when unset/invalid.
func (MigrationSafetyConfig) EffectiveVolatileDefault ¶
func (c MigrationSafetyConfig) EffectiveVolatileDefault() string
EffectiveVolatileDefault returns the configured severity for the volatile-default rule, falling back to "warn" when unset/invalid.
func (MigrationSafetyConfig) IsEnabled ¶
func (c MigrationSafetyConfig) IsEnabled() bool
IsEnabled reports whether migration safety linting is on. Nil Enabled means "on by default" so opt-in is implicit.
type ORMConfig ¶ added in v1.0.0
type ORMConfig struct {
// Package is the project-relative directory the ORM projects into —
// every <entity>_orm_gen.go, store_gen.go and _repo_ext.go seam — and
// so the Go package they declare (its last path element). Empty means
// DefaultORMPackage.
//
// It exists for a project that already has a data layer of its own in
// internal/db: there the projection's names (Chat, Message, Store,
// NewStore) collide with the project's, one package cannot declare
// both, and porting onto the store one table at a time needs the two
// to live side by side. `package: internal/store` puts the projection
// in its own package; every generated consumer (CRUD wiring, the store
// Deps in compose.go, factories, shapes, the drift guard) follows it.
Package string `yaml:"package,omitempty"`
}
ORMConfig is the generated data layer's placement.
func ORMPackage ¶ added in v1.0.0
func ORMPackage(cfg *ProjectConfig) ORMConfig
ORMPackage returns the ORM placement of cfg, the default for a nil cfg — the one accessor every consumer reads, so a project with no forge.yaml and one with no `database.orm` block land in the same place.
func (ORMConfig) PackageDir ¶ added in v1.0.0
PackageDir is the ORM's project-relative directory, slash-separated.
func (ORMConfig) PackageName ¶ added in v1.0.0
PackageName is the Go package name the ORM's files declare: the last element of PackageDir.
type ObservabilityConfig ¶ added in v0.1.45
type ObservabilityConfig struct {
// LogLevel is the slog level at which the LogMiddleware records
// SUCCESSFUL component calls ("debug" | "info" | "warn" | "error").
// Every success is recorded unless the seam appends
// observe.WithSuccessSampling to LogMiddleware, which samples them per
// method. Failures always log at Error regardless. Default "debug"
// keeps success logging quiet under a production Info handler. An
// unknown value falls back to the default.
LogLevel string `yaml:"log_level,omitempty"`
}
ObservabilityConfig seeds the OWNED per-package observe_chain.go seam (the in-process component middleware chain the generated decorator routes through). It is a SCAFFOLD-TIME default: `forge scaffold` reads it to stamp the initial chain; the seam is user-owned afterward, so editing this block does not retro-rewrite existing seams — it only changes what newly scaffolded packages start with.
func (ObservabilityConfig) SlogLevelExpr ¶ added in v0.1.45
func (o ObservabilityConfig) SlogLevelExpr() string
SlogLevelExpr resolves LogLevel to the Go expression for the matching slog.Level constant, used to stamp the observe_chain.go seam. Unknown or empty values default to slog.LevelDebug (quiet-on-success).
type PlanEntity ¶
type PlanEntity struct {
Name string `yaml:"name" json:"name"` // PascalCase message name, e.g. "Project"
TableName string `yaml:"table_name,omitempty" json:"table_name,omitempty"` // override; defaults to pluralized snake_case
SoftDelete bool `yaml:"soft_delete,omitempty" json:"soft_delete,omitempty"`
Timestamps bool `yaml:"timestamps,omitempty" json:"timestamps,omitempty"`
// AppendOnly marks an immutable ledger: the table is declared
// `forge:append-only` in its COMMENT ON TABLE, so the generated store
// omits Update/UpdateMasked/Delete entirely. The declaration lives in
// the MIGRATION (schemadef.TableMarkerAppendOnly) — the proto marker of
// the same name is a birth-time instruction that nothing carries
// forward, which is why the store used to expose mutators for a table
// postgres rejects every write to.
AppendOnly bool `yaml:"append_only,omitempty" json:"append_only,omitempty"`
Fields []PlanEntityField `yaml:"fields" json:"fields"`
// Constraints are the table's named UNIQUE / CHECK / FOREIGN KEY
// constraints, which the ORM generator publishes as per-entity
// constants so a service can branch on orm.ConstraintName(err) without
// hardcoding the identifier. The PRIMARY KEY is deliberately absent:
// inserting a duplicate id is a forge-internal concern (pkg/crud
// generates the ULID), so no service branches on it, and emitting a
// constant nobody reads is noise in every generated file.
Constraints []PlanEntityConstraint `yaml:"constraints,omitempty" json:"constraints,omitempty"`
// ReadOnly marks a view entity (codegen.EntityDef.ReadOnly): the store
// emits List/Count and Get by its key, and no Create/Update/Delete.
ReadOnly bool `yaml:"read_only,omitempty" json:"read_only,omitempty"`
}
PlanEntity describes a database entity to scaffold.
type PlanEntityConstraint ¶ added in v0.1.45
type PlanEntityConstraint struct {
Name string `yaml:"name" json:"name"`
Kind ConstraintKind `yaml:"kind" json:"kind"`
// Columns are the columns the constraint spans, in key order. It is
// what makes the generated doc comment say WHICH columns must be
// distinct rather than just naming the constraint back to the reader.
Columns []string `yaml:"columns,omitempty" json:"columns,omitempty"`
}
PlanEntityConstraint is one named constraint on an entity's table, as the APPLIED schema knows it. Name is what postgres reports in a violation's structured constraint field — the value orm.ConstraintName(err) returns — so it must be carried through verbatim rather than re-derived.
type PlanEntityField ¶
type PlanEntityField struct {
Name string `yaml:"name" json:"name"` // snake_case proto field name
Type string `yaml:"type" json:"type"` // "string", "int64", "bool", "google.protobuf.Timestamp"
PrimaryKey bool `yaml:"primary_key,omitempty" json:"primary_key,omitempty"`
NotNull bool `yaml:"not_null,omitempty" json:"not_null,omitempty"`
Unique bool `yaml:"unique,omitempty" json:"unique,omitempty"`
Default string `yaml:"default,omitempty" json:"default,omitempty"`
References string `yaml:"references,omitempty" json:"references,omitempty"` // "users.id"
// Generated marks a GENERATED ALWAYS AS (...) STORED column: the DB
// computes it, so the ORM emits ,scanonly (never written on
// INSERT/UPDATE).
Generated bool `yaml:"generated,omitempty" json:"generated,omitempty"`
// Identity marks a GENERATED ... AS IDENTITY column. Postgres reports no
// DEFAULT for one, so with Default it is what says the database
// allocates a key (the ORM's ,autoincrement) — never the key's type.
Identity bool `yaml:"identity,omitempty" json:"identity,omitempty"`
// Secret marks a column backed by a `// forge:secret` wire field: the
// read path never packs it (a client always reads it back as ""), so the
// generated repo's Spec.SecretColumns preserves it on a maskless
// full-replace Update rather than clobbering the stored value with "".
Secret bool `yaml:"secret,omitempty" json:"secret,omitempty"`
// Immutable marks a column declared `forge:immutable` in a COMMENT ON
// COLUMN. It projects to Bun's `skipupdate` tag option, so a full-replace
// UPDATE omits it from the SET clause while an explicit update_mask
// naming it still writes it.
//
// The declaration lives in the MIGRATION, not the proto: whether a column
// may be rewritten is a fact about storage, and the wire contract and the
// schema evolve on independent clocks. Inferring it from the absence of a
// wire field is wrong — a column can be absent from the API and still be
// ordinary mutable state (an internal score, a denormalized cache).
Immutable bool `yaml:"immutable,omitempty" json:"immutable,omitempty"`
// Version marks a column declared `forge:version` in a COMMENT ON
// COLUMN. It opts the entity into optimistic concurrency control:
// forge/pkg/crud's Repo adds it to Update/UpdateMasked's WHERE clause
// (matched against the caller's last-read value) and increments it on
// a successful write, failing a lost race with svcerr.ErrAborted
// rather than silently overwriting a concurrent writer's change.
Version bool `yaml:"version,omitempty" json:"version,omitempty"`
// FillStrategy carries a `forge:fill=<strategy>` COMMENT ON COLUMN
// declaration verbatim ("ulid" or "handler"), "" for none. "ulid" makes
// forge/pkg/crud's Repo generate one at Create for this (non-PK) column,
// the same chokepoint that already ULID-generates an empty string PK.
// "handler" changes no codegen behavior — it only suppresses the
// unsatisfiable-column lint. See schemadef.ColumnMarkerFill.
FillStrategy string `yaml:"fill_strategy,omitempty" json:"fill_strategy,omitempty"`
// GoType carries a `forge:go-type=<spec>` COMMENT ON COLUMN declaration
// verbatim (codegen.ParseGoTypeOverride): the struct field's Go type in
// place of the projected one. "" for none.
GoType string `yaml:"go_type,omitempty" json:"go_type,omitempty"`
// ZeroRejected marks a column a CHECK keeps from holding its Go zero
// (codegen.EntityColumn.ZeroRejected): its zero is "unset", so a DEFAULT
// applies through ,nullzero rather than a pointer field.
ZeroRejected bool `yaml:"zero_rejected,omitempty" json:"zero_rejected,omitempty"`
}
PlanEntityField describes a field on an entity.
type PlanField ¶
type PlanField struct {
Name string `yaml:"name" json:"name"`
Type string `yaml:"type" json:"type"`
}
PlanField describes a field in a proto message.
type PlanFile ¶
type PlanFile struct {
ProjectName string `yaml:"project_name"`
GoModule string `yaml:"go_module"`
GoVersion string `yaml:"go_version,omitempty"`
MockData bool `yaml:"mock_data,omitempty"`
Services []PlanService `yaml:"services,omitempty"`
Packages []PlanPackage `yaml:"packages,omitempty"`
Frontends []PlanFrontend `yaml:"frontends,omitempty"`
Entities []PlanEntity `yaml:"entities,omitempty" json:"entities,omitempty"`
}
PlanFile represents a forge plan for batch scaffolding.
type PlanFrontend ¶
type PlanFrontend struct {
Name string `yaml:"name"`
Kind string `yaml:"kind,omitempty"` // "mobile" for React Native; empty/default = Next.js web
}
PlanFrontend describes a frontend to scaffold.
type PlanPackage ¶
type PlanPackage struct {
Name string `yaml:"name"`
Kind string `yaml:"kind,omitempty"` // "eventbus", "client", or empty
Description string `yaml:"description,omitempty"`
}
PlanPackage describes an internal package to scaffold.
type PlanRPC ¶
type PlanRPC struct {
Name string `yaml:"name" json:"name"`
Description string `yaml:"description,omitempty" json:"description,omitempty"`
Request []PlanField `yaml:"request,omitempty" json:"request,omitempty"`
Response []PlanField `yaml:"response,omitempty" json:"response,omitempty"`
}
PlanRPC describes an RPC to scaffold in a service proto.
type PlanService ¶
type PlanService struct {
Name string `yaml:"name" json:"name"`
Description string `yaml:"description,omitempty" json:"description,omitempty"`
RPCs []PlanRPC `yaml:"rpcs,omitempty" json:"rpcs,omitempty"`
}
PlanService describes a service to scaffold.
type ProjectConfig ¶
type ProjectConfig struct {
Name string `yaml:"name"`
ModulePath string `yaml:"module_path,omitempty"`
// Kind is the project shape (service|cli|library). It is not a forge.yaml
// field: the loader DERIVES it from the project's real sources — the KCL
// deploy tree, the pkg/app composition root, internal/handlers/, the
// service protos, and cmd/<name>/main.go (see deriveProjectKindFromSources).
// The yaml tag is "-" so a stale `kind:` in forge.yaml is reported with a
// migration hint instead of being honored.
Kind string `yaml:"-"`
Binary string `yaml:"binary,omitempty"` // "per-service" (default), "shared" — one Go binary, cobra subcommand per service
// ForgeVersion records the forge binary version that this project's
// generated artifacts were last produced against. It is set at
// `forge project new` time, bumped after a successful `forge project upgrade`, and
// consulted by `forge generate` to warn when the forge binary on
// PATH has drifted from the version pinned by the project. Empty
// (legacy) projects are treated as "0.0.0".
ForgeVersion string `yaml:"forge_version,omitempty"`
// Harness records which AI harness this project was scaffolded for
// (`forge project new --harness`). It is the ONLY durable record of that
// choice: the flag is read once at scaffold time, and without it on disk
// every later `forge generate` has to guess. It selects the harness
// pointer files (CLAUDE.md, .cursorrules, ...) written beside AGENTS.md,
// which every harness gets. Forge never writes skill bodies for any
// harness. Empty means reliant.
Harness string `yaml:"harness,omitempty"`
// NOTE: there is intentionally NO project-level `hot_reload:` field.
// The single hot-reload switch is `features.hot_reload`, which the dev
// loop and the air-config codegen actually read; a second top-level key
// spelled the same way resolved through a different accessor that no
// caller ever invoked. A `hot_reload:` key at the top level is reported
// with a migration hint (see removedSchemaKeys).
//
// NOTE: there is intentionally NO `components:` field, in yaml or in
// memory. What a project builds and runs — Connect servers, workers,
// crons, operators, secondary binaries — is declared by the code itself
// (the proto descriptor, internal/workers/, internal/operators/, cmd/),
// and a config object carrying a copy would be a second source of truth
// that is empty exactly when nobody remembered to fill it. A caller that
// needs the component inventory asks the project for it:
// codegen.DiscoverProjectComponents. A `components:` key in forge.yaml is
// reported with a migration hint (see removedSchemaKeys).
//
// NOTE: there is intentionally NO `packages:` field either. An internal
// package is declared by internal/<name>/contract.go — the file the
// bootstrap/injector codegen has always walked for — and its
// outbound-boundary claim by the `//forge:outbound-io` marker in its own
// source. The retired list was never a codegen input: deleting it left
// every package still imported, constructed and middleware-wrapped in
// internal/app/compose.go. It steered only the REPORTERS, so a stale
// entry made `forge project map` / `forge project audit` / the
// architecture doc name a package that does not exist while hiding one
// that does. They now resolve through codegen.DiscoverInternalPackages.
// A `packages:` key is reported with a migration hint (see
// removedSchemaKeys).
// Frontends is the frontend inventory. It is DERIVED, never read from
// forge.yaml (there is no `frontends:` key): the loader fills it from the
// frontends found on disk, and `forge generate` overlays the KCL
// `forge.Frontend` declarations onto it. See frontend_inventory.go.
Frontends []FrontendConfig `yaml:"-"`
// The section blocks below are all omitempty: a freshly scaffolded
// forge.yaml leaves them absent and the loader fills shape-derived
// defaults (see ApplyDerivedDefaults in derive.go). A present block
// is taken literally — write the block (or a single key) to override.
Database DatabaseConfig `yaml:"database,omitempty"`
CI CIConfig `yaml:"ci,omitempty"`
// NOTE: there is intentionally NO `build:` field. Build is declared
// per-service, per-env in KCL (the polymorphic Build union —
// GoBuild | DockerBuild | ShellBuild on each forge.Service); forge.yaml
// carries zero build config. A `build:` key in forge.yaml is REJECTED
// as an unknown key by the strict loader (walkUnknownKeys) rather than
// silently ignored — version stamping moves to a KCL GoBuild.ldflags
// `-X` entry.
Deploy DeployConfig `yaml:"deploy,omitempty"`
Docker DockerConfig `yaml:"docker,omitempty"`
K8s K8sConfig `yaml:"k8s,omitempty"`
Lint LintConfig `yaml:"lint,omitempty"`
Contracts ContractsConfig `yaml:"contracts,omitempty"`
// Config steers humans and LLMs away from reading the environment
// directly (os.Getenv / os.LookupEnv / os.Environ) and toward forge's
// generated, dependency-injected typed config object. See
// [ConfigGuardConfig] for the enforce_typed_access / loader_package
// semantics and the absent-key default (warn).
Config ConfigGuardConfig `yaml:"config,omitempty"`
// NOTE: there is intentionally NO `auth:` block. Authentication is owned
// code — internal/app/auth.go's SetupAuth picks the validator, and
// issuer/audience/JWKS are per-deployment values that live in env vars
// routed through KCL. An `auth:` key is reported with a migration hint
// (see removedSchemaKeys).
// Features is DERIVED from what exists in the repo (see
// DeriveFeatureDefaults). The optional `features:` block pins a feature
// on or off whatever the tree says — the switch for a project whose
// shape implies a feature it does not want (see FeaturesConfig).
Features FeaturesConfig `yaml:"features,omitempty"`
// Observability seeds the OWNED per-package observe_chain.go seam that a
// generated component decorator routes through. The values are read at
// `forge scaffold` time to stamp the initial chain (log level, etc.);
// the seam is user-owned thereafter. See [ObservabilityConfig].
Observability ObservabilityConfig `yaml:"observability,omitempty"`
// API toggles project-level API protocol skins layered on top of the
// Connect mux. Default zero-value leaves both REST and OpenAPI off so
// existing projects regenerate identically. See [APIConfig] for the
// per-field semantics.
API APIConfig `yaml:"api,omitempty"`
// DevStack bounds forge's parallel-dev-stack port allocation — how many
// stacks can run side by side, and how far apart their port blocks sit.
// See [DevStackConfig] for why this has to be declared rather than
// inferred.
DevStack DevStackConfig `yaml:"dev_stack,omitempty"`
// Generate declares commands `forge generate` runs for the project —
// the escape hatch for a generator forge does not own. See
// [GenerateConfig] / [GenerateHook]; read by the "generate hooks
// (forge.yaml)" pipeline step.
Generate GenerateConfig `yaml:"generate,omitempty"`
// Upgrade declares what `forge project upgrade` does beyond forge's own
// codemods and playbooks: today, how to re-vendor a contract copied from
// another repository. See [UpgradeConfig]; read by upgrade_converge.go.
Upgrade UpgradeConfig `yaml:"upgrade,omitempty"`
// contains filtered or unexported fields
}
ProjectConfig represents the forge.yaml file. Fields align with proto/forge/project/v1/project.proto.
func LoadProject ¶
func LoadProject(data []byte, path string) (*ProjectConfig, error)
LoadProject is THE forge.yaml loader: it parses a forge.yaml byte stream into a ProjectConfig with strict validation, derives the project kind from the project's real sources, and applies the shape-derived section defaults. Both the CLI loader and the generator's ReadProjectConfig route through it, so the load rules live in exactly one place.
Unknown keys (typos, dropped fields) and missing required fields are reported in a single error rather than silently succeeding or failing on the first issue.
path locates the project on disk: its directory is read to derive the project kind, and it prefixes error messages. It is never opened for the config bytes themselves — the caller supplies those. A path with no on-disk project behind it (byte-only loads) loads as a library.
Behaviour:
- The YAML is decoded into a yaml.Node tree, then walked against the ProjectConfig struct shape. Unknown keys are collected with their YAML line number and parent path; a Levenshtein-based suggestion is attached when a known sibling key is within edit distance 2 (or 3 for keys >= 8 chars).
- The same bytes are then decoded into a ProjectConfig via the standard yaml decoder so that scalar-type mismatches (e.g. port: "8080") surface as their own error class.
- Required-field validation runs on the populated struct.
All issues across the three phases are batched into a single ValidationError; the caller sees the full list rather than just the first failure.
What a project CONTAINS is not part of this: components are read from the code that declares them (codegen.DiscoverProjectComponents), never from forge.yaml and never from a field on the returned config.
func LoadProjectDir ¶ added in v0.1.45
func LoadProjectDir(projectDir string) (*ProjectConfig, error)
LoadProjectDir reads <projectDir>/forge.yaml and loads it through LoadProject. It is the ONLY sanctioned way to answer a forge.yaml question from a directory path.
Callers that need one optional toggle mid-codegen used to hand-roll a line scan over the file rather than take this dependency, each one justified by an import cycle that does not exist (internal/config imports only internal/naming). Those scans agreed with the real parser only for the exact block-style spelling forge itself emits: flow style (`api: {rest: true}`), a nested key indented under a sibling, or an anchor/alias all silently read as "absent". Route through here instead and treat a load error as "the caller's default" — a broken forge.yaml has already failed the command that loaded it first.
func NormalizeForWrite ¶
func NormalizeForWrite(c *ProjectConfig) *ProjectConfig
NormalizeForWrite returns a copy of c with every derivable value that matches its shape-derived default removed, so marshalling produces the minimal forge.yaml. Explicit values that DIFFER from derivation are preserved — overrides survive round-trips; boilerplate does not.
Dropping a value that equals its derived default is behavior-preserving by construction: the loader re-derives the identical value on the next read. The original c is not mutated.
func (ProjectConfig) EffectiveBinary ¶
func (c ProjectConfig) EffectiveBinary() string
EffectiveBinary returns the binary mode, defaulting to "per-service" so legacy forge.yaml files without the field keep producing the canonical cmd/server.go shape.
func (ProjectConfig) EffectiveForgeVersion ¶
func (c ProjectConfig) EffectiveForgeVersion() string
EffectiveForgeVersion returns the forge version pinned by this project, defaulting to "0.0.0" for legacy projects that predate the field. Callers can use the "0.0.0" sentinel to detect "no baseline yet" and nudge the user toward `forge project upgrade`.
func (ProjectConfig) EffectiveKind ¶
func (c ProjectConfig) EffectiveKind() string
EffectiveKind returns the project kind, defaulting to "service".
func (*ProjectConfig) HasGoModule ¶ added in v1.0.0
func (c *ProjectConfig) HasGoModule() bool
HasGoModule reports whether the project is a Go module: a go.mod at its root. A static site — its frontends and their deploy envs, nothing else (`forge project new --kind site`) — has none, so every Go step (codegen, go mod tidy, goimports, the compile check, golangci-lint, the contract linter) has nothing to act on and is skipped, naming this reason, rather than failing on `pattern ./...`. A site that grows a backend gains a go.mod and every one of those steps with it.
With no on-disk project (a byte-only load in a test) the answer is yes: every kind but a site is a Go module, and a site is only ever read from disk.
func (ProjectConfig) HasReactNativeFrontend ¶
func (c ProjectConfig) HasReactNativeFrontend() bool
HasReactNativeFrontend reports whether any frontend in the project is a React Native (Expo) app. Used to gate features that only apply to mobile — e.g. the `@<scope>/ui-native` workspace package.
Returns true for frontends declared with `type: react-native` (or the historic `type: react_native` underscore form the validator also accepts).
func (ProjectConfig) IsBinaryShared ¶
func (c ProjectConfig) IsBinaryShared() bool
IsBinaryShared reports whether the project uses the shared-binary codegen mode (one Go binary, cobra subcommand per service, KCL MultiServiceApplication for deploy).
func (ProjectConfig) IsCLIKind ¶
func (c ProjectConfig) IsCLIKind() bool
IsCLIKind reports whether the project is a CLI binary (no server scaffolding).
func (ProjectConfig) IsFrontendWorkspacesEnabled ¶
func (c ProjectConfig) IsFrontendWorkspacesEnabled() bool
IsFrontendWorkspacesEnabled reports whether the project uses the pnpm-workspaces layout (root pnpm-workspace.yaml + packages/api + packages/hooks shared across frontends). It is read off the tree, not configured: see DeriveFrontendWorkspaces.
func (ProjectConfig) IsLibraryKind ¶
func (c ProjectConfig) IsLibraryKind() bool
IsLibraryKind reports whether the project is a pure Go library (no cmd/).
func (ProjectConfig) IsServiceKind ¶
func (c ProjectConfig) IsServiceKind() bool
IsServiceKind reports whether the project is a Connect-RPC service.
func (*ProjectConfig) ToolchainFrontends ¶ added in v0.1.45
func (c *ProjectConfig) ToolchainFrontends() []FrontendConfig
ToolchainFrontends returns the frontends forge owns a Node toolchain for: every frontend in the inventory with its path resolved. It is the single project-level answer to "which frontends may forge shell into, and where do they live" — shared by the build target resolution and the lint pipeline's frontend lane so the two can never disagree about the set. Command-specific filters (--target, KCL deploy mode, per-command skips) layer on top of this set; they do not re-derive it.
A cross-repo frontend whose source has not been materialized yet is EXCLUDED rather than given the conventional fallback. This set is the one forge shells into — `npm run build`, `tsc` — and such a frontend has no directory in this tree by design. Naming it frontends/<name> invents a path that does not exist and never will, so the command fails deep inside npm against a fabricated directory instead of simply not attempting a frontend whose code is not on disk. Callers that DO materialize sources (forge build) rewrite Path first, so the frontend is back in this set by the time they read it.
Entries are returned AS THEY ARE. Since resolution happens once, at the config load seam (ResolveInventoryAtLoad), a path read here is already the resolved one and this function does not re-derive it — which is what keeps a RESOLVED path from becoming indistinguishable from a DECLARED one. Callers read the directory through Dir, which applies the frontends/<name> convention for a config assembled in memory rather than loaded from disk.
func (ProjectConfig) WithFrontendWorkspaces ¶ added in v0.1.45
func (c ProjectConfig) WithFrontendWorkspaces(on bool) ProjectConfig
WithFrontendWorkspaces returns a copy that uses (or does not use) the pnpm-workspaces layout, for a config assembled in memory rather than loaded from a tree that already shows the layout.
type Service ¶
type Service interface {
// EffectiveKind normalizes a raw kind string to one of the canonical
// ProjectKind* constants, defaulting to "service" for empty/unknown input.
EffectiveKind(kind string) string
}
Service is the behavioral surface of the config package.
Today it wraps EffectiveProjectKind so the require-contract analyzer is satisfied without forcing the data-type accessor methods onto an interface they have no business being on. Future stateful behavior (file loading, validation, defaulting against forge.yaml on disk) will land here as internal/cli and internal/generator are ported and their config-touching helpers consolidate into this package.
type TracingLintConfig ¶ added in v1.0.0
type TracingLintConfig struct {
// LogWrappers names the project's own context-free logging helpers —
// a `logging.Info(msg, args...)` that calls slog.Info — so
// forge-tracing-log-without-ctx flags and rewrites their calls the way
// it does slog's own.
LogWrappers []LogWrapperConfig `yaml:"log_wrappers,omitempty"`
}
TracingLintConfig configures `forge lint --tracing`.
type UpgradeConfig ¶ added in v1.0.0
type UpgradeConfig struct {
// VendoredContracts declare contracts the project vendors from another
// repository, and how to catch the vendored copy up. See
// [VendoredContract].
VendoredContracts []VendoredContract `yaml:"vendored_contracts,omitempty"`
}
UpgradeConfig is the `upgrade:` block: what `forge project upgrade` does for the project beyond forge's own codemods and playbooks.
type ValidationError ¶
type ValidationError struct {
Path string
Issues []validationIssue
}
ValidationError aggregates all forge.yaml validation issues into a single error so callers see the full picture instead of fail-fast on the first problem. Implements error.
func (*ValidationError) Error ¶
func (e *ValidationError) Error() string
type VendoredContract ¶ added in v1.0.0
type VendoredContract struct {
// Name labels the contract in upgrade's output. Required and unique.
Name string `yaml:"name"`
// Report is the project-relative path of the lag report the project's own
// check writes.
Report string `yaml:"report"`
// Schema is the report's expected `schema` value. A report claiming
// another is not trusted.
Schema string `yaml:"schema"`
// EnvPath is a dotted path to an object in the report whose string values
// become environment variables for the commands (for example
// "revendor.env").
EnvPath string `yaml:"env_path,omitempty"`
// Commands are the argv lists that re-vendor, run in order from the
// project root. No shell: spell a pipeline as [sh, -c, "..."].
Commands [][]string `yaml:"commands"`
}
VendoredContract is a contract (protos, a schema, generated clients) the project copies from a repository it does not own, with a check that reports how far the copy lags and a command sequence that catches it up.
forge does not know what is vendored or from where. After a forge bump the vendored copy is what most often goes stale, so `forge project upgrade` reads the check's report and, when it says the copy lags, runs the declared commands before it regenerates.
upgrade:
vendored_contracts:
- name: controlplane
report: tmp/ci/reliant-vendored-proto.json
schema: reliant-vendored-proto/v1
env_path: revendor.env
commands:
- [node, .github/scripts/sync-controlplane-proto.mjs]
- [npm, run, "proto:generate:controlplane"]
The report is a JSON document with a top-level `status` that is "in-sync" or "lag" (any other value, or a `schema` other than the one declared, is handed to a human rather than guessed at). `env_path` optionally names an object in the report, as a dotted path, whose string values are exported to the commands.