Documentation
¶
Overview ¶
Package release is forge's release ledger vocabulary: what a RELEASE is (an immutable, content-addressed set of artifacts under a version label), what a PROMOTION is (one append-only event binding an environment to a release), and the invariants both must hold no matter where they are stored.
One model, two backends ¶
forge records releases and promotions in two places, and they are the SAME model:
- the file backend, inside a project: .forge/releases/<version>.json and one append-only .forge/promotions/<env>.jsonl per environment;
- a hosted control plane, over its DeployService RPCs, when an environment declares forge.ControlPlane.
Both backends read and write these types, and both enforce the rules in this package. The hosted side additionally enforces them structurally in its database (CHECKs, append-only triggers); the Go rules here are the ones a directory of files can hold, and they are the acceptance test for the hosted side, not a weaker copy of it.
Why it is public ¶
A hosted ledger is implemented OUTSIDE forge (Reliant's control plane is one), and it must speak exactly this vocabulary or the two backends drift into two products. Declaring the types once, in an importable package, is what makes "the hosted ledger stores what forge cut" a compile-time fact rather than a comment in two repos.
Closed enums, validated on read ¶
Kind, Mode, PromotionKind, GateStatus, StageKind and StageStatus are CLOSED. An unknown or empty value fails to decode rather than defaulting to something plausible — an artifact whose kind nobody recognises must not be read as an image, and a promotion whose kind is unknown must not be read as a forward move. The earlier "empty kind means OCI" convention is gone: every ledger states what it holds.
Evidence and runs ¶
A Gate is one check's result attached to a promotion, and it is EVIDENCE, not enforcement: nothing in this package refuses a promotion on a gate's status. What a gate buys is attribution — this principal claimed this verdict at this time, with a link to the full report — which is the most a recorded claim can ever prove. A gate that must BLOCK is checked before the entry is written.
A Run is the identity of one pipeline attempt, and it is the join key that turns scattered ledger rows into a RunTimeline. There is no run entity: every Stage is either derived from the ledger (the cut, the promotions, each promotion's rollout) or recorded as a gate (build, lint, test, smoke, wait). A parallel event log would be a second source of truth free to disagree with the ledger, so there is none.
One closed set, two parse paths ¶
Enum values arrive from two places, and conflating them is how a ledger becomes unreadable:
- The WRITE path is strict. ParseGateStatus, ParseStageKind and ParseStageStatus refuse anything outside the set, so a typo in a --gate flag fails before anything is recorded.
- The READ path is lenient where history demands it. GateStatusFromStored maps a status written before the set closed to GateStatusError and keeps the original on Gate.RawStatus. Old entries stay renderable, an uninterpretable claim never renders as a pass, and nothing is silently discarded.
Gate.Validate closes the loop: a gate carrying a RawStatus can be read but not written, so leniency on read never becomes leniency on write.
Index ¶
- Constants
- Variables
- func BundleClusterPath(cluster string) string
- func BundlePathSegment(s string) string
- func CanonicalRepo(url string) string
- func CheckRecut(existing, requested Release) error
- func ConfigDigest(shape Shape) (string, error)
- func HashDocument(doc any) (string, error)
- func HostID() string
- func ImagesIn(renderedDoc string, pins map[string]string) map[string]string
- func IsNestedCheckout(dir string) bool
- func NormalizeImages(doc any, pins map[string]string) any
- func ObjectHashes(doc any, pins map[string]string) (hash, configHash string, err error)
- func RecreatableKind(kind string) bool
- func RedactSecrets(doc any) any
- func StatefulKind(kind string) bool
- func ValidDigest(d string) bool
- func ValidObjectID(s string) bool
- type Actor
- type Apply
- type ApplyOutcome
- type ApplyState
- type ApplyStatus
- type ApplyWorkload
- type Artifact
- type Attest
- type BuildSource
- type BundleClusterTree
- type BundleDoc
- type BundlePins
- type BundleRecord
- type BundleSecretRef
- type CaptureOptions
- type Changes
- type DriftObservation
- type EnvKind
- type EnvRecord
- type FindingClass
- type Gate
- type GateStatus
- type Git
- type ImageChange
- type Kind
- type KindChange
- type LocalSession
- type Mode
- type ObjectChange
- type ObjectKey
- type Plan
- type PlanBasis
- type PlanFinding
- type PlanInput
- type Promotion
- type PromotionKind
- type Provenance
- type Release
- type Run
- type RunTimeline
- type RuntimeChange
- type Shape
- type ShapeAlert
- type ShapeAlertRoute
- type ShapeControlPlaneSecret
- type ShapeDiff
- type ShapeObject
- type ShapeObservabilityPolicy
- type ShapeSecret
- type ShapeWorkload
- type Source
- type Stage
- type StageKind
- type StageStatus
- type Worktree
Constants ¶
const ( BundleArtifactType = "application/vnd.forge.bundle.v1" BundleConfigMediaType = "application/vnd.forge.bundle.config.v1+json" BundleManifestsLayer = "application/vnd.forge.bundle.manifests.v1.tar+gzip" // RedactedSecretPrefix marks a Secret value replaced by its hash before a // render is hashed or packaged: "forge.dev/redacted: sha256:<hex>". RedactedSecretPrefix = "forge.dev/redacted: " )
Bundle OCI media types. One bundle is one OCI image manifest whose config blob is the BundleDoc and whose single layer is the manifest stream.
THE MANIFEST LAYER IS WHAT FLUX APPLIES. A Flux OCIRepository selects it by media type and `operation: extract`, which untars it into the source controller's artifact; a Kustomization then applies the YAML under `spec.path`. So the layer's media type and its internal layout are a CONTRACT with the reconciler, not an internal detail — see BundleClusterPath.
There is deliberately no charts layer. A second layer would be invisible to Flux (a layerSelector picks exactly one), so anything put there would be recorded as shipped and never applied. Platform charts are a cluster-bootstrap concern and stay off the bundle entirely; see internal/bundle's package doc.
const ( BundleObservabilityPrefix = "observability" BundleDashboardsPath = BundleObservabilityPrefix + "/dashboards" BundleMetricsSchemaPath = BundleObservabilityPrefix + "/metrics-schema.json" )
The observability files a bundle carries beside its manifests (console v2 contract K4), in the SAME layer, under a top-level directory the reconciler never applies.
WHY THE SAME LAYER AND NOT A SECOND ONE. A Flux layerSelector selects exactly one layer, so a second layer is invisible to the reconciler — which is right for data nothing applies, but it would also change what a bundle IS (one manifest, one config, one layer) for every reader that counts on that, and Fetch refuses a layer it does not know. A Kustomization's spec.path is always BundleClusterPath, under BundleManifestsPrefix, so a sibling directory in the extracted artifact is never reached by anything that applies.
observability/dashboards/<name>.json one renderer-neutral dashboard each observability/metrics-schema.json the metrics schema (forge.metrics/v1)
A dashboard file is named <name>_gen.json when forge generated it and <name>.json when the project owns it; a project's file wins over the generated one of the same name, so a name never appears twice.
const ( // AlertSourceUser is a rule the project declares (forge.AlertRule). AlertSourceUser = "user" // AlertSourceDefault is one of forge's generated defaults. AlertSourceDefault = "default" )
Alert rule sources.
const ( AlertReceiverInApp = "reliant-inapp" AlertReceiverSlack = "slack" )
The receivers every control plane provides, and the routes a shape carries when the author declares none: in-app and Slack, never a pager.
const ( ObsCategoryTraces = "traces" ObsCategoryMetrics = "metrics" ObsCategoryLogs = "logs" ObsCategoryProfiles = "profiles" ObsCategoryMetadata = "metadata" ObsCategoryBrowser = "browser" )
Observability opt-out categories: the closed vocabulary of ShapeObservabilityPolicy.Disabled. "metadata" is customer metadata, the per-workload container data the platform's node agent collects (pod CPU/memory, restarts, last exit reason, events, image and version).
const ( IdentityType = "type" IdentityLoadBalancerIP = "load_balancer_ip" IdentityLoadBalancerClass = "load_balancer_class" IdentityStaticIP = "static_ip" )
Identity keys. Closed.
const ( CaptureTreeTimeout = 5 * time.Second CaptureMaxDirtySize = 2 << 30 // bytes of untracked-unignored + modified content )
Capture budget for the dirty-tree hash (F-16). Past either bound the tree is recorded as unhashed rather than making a developer wait: a clean tree's hash is HEAD^{tree} and costs nothing, so the bound only ever applies to a dirty checkout, which no protected path relies on.
const ( FindingObjectAdded = "object_added" FindingObjectChanged = "object_changed" FindingObjectRemoved = "object_removed" FindingImageChanged = "image_changed" FindingConfigChanged = "config_changed" FindingSecretNeeded = "secret_needed" FindingStatefulDeletion = "stateful_deletion" FindingLBIdentityChange = "lb_identity_change" FindingDrift = "drift" // FindingUnknown: a section could not be computed (no recorded live // config, drift not observable, secret presence not readable). It is a // WARN, never silence: a blank section must not read as "nothing // changes" (F-20). FindingUnknown = "unknown" )
Finding codes. Closed: a consumer switches on them, and a stop-class code is what an operator names to acknowledge it.
const ( SectionObjects = "objects" SectionStateful = "stateful_deletions" SectionLB = "lb_identity" SectionImages = "images" SectionConfig = "config" SectionSecrets = "secrets" SectionDrift = "drift" )
Plan sections, in display order (§8.6).
const ApplierForge = "forge"
ApplierForge is Shape.Applier for an env forge applies by declaration.
const ArtifactPlaceholder = "forge.dev/artifact:"
ArtifactPlaceholder is what a release-bound image digest becomes in the document a ConfigHash is taken over: `forge.dev/artifact:<key>`.
const BundleHostedCluster = "hosted"
BundleHostedCluster is the cluster key under which a bundle carries an env's HOSTED tier records (forge.dev Workload / ManagedDatabase / StaticSite).
Hosted workloads run on the platform's own clusters, which forge cannot name as a kube context, so they live under ONE well-known tree, BundleClusterPath(BundleHostedCluster). The control plane maps that tree to the placement it resolved for the environment; forge never says where.
const BundleManifestsPrefix = "manifests"
BundleManifestsPrefix is the directory the manifest layer's entries live under, inside the extracted layer. Every entry is at `<prefix>/<cluster>/<file>.yaml`.
const BundleSchema = "forge.dev/bundle/v1"
BundleSchema is the BundleDoc schema this package reads and writes.
const ( // ControlPlaneSecretTelemetryToken is the per-environment OTLP ingest // token. ControlPlaneSecretTelemetryToken = "telemetry_token" )
Control-plane secret kinds: what the control plane mints into the Secret.
const MaxShapeBytes = 256 << 10
MaxShapeBytes bounds a shape's canonical JSON. It is what a hosted ledger indexes per bundle, and what Live reads per env in one round trip, so it is a summary and never the render itself.
const MinAlertInterval = time.Minute
MinAlertInterval is the evaluation-interval floor a shape may record.
const ObsOptInReplay = "replay"
ObsOptInReplay is session replay: the browser SDK's recorded DOM events, dropped by the platform unless the env opts in.
const SessionStaleAfter = 3 * time.Minute
SessionStaleAfter is how long a live session may go unseen before a reader greys it out. Heartbeats are every 60s.
SharedVariant is the sole Digests key of a Mode ModeShared artifact: one digest serves every environment.
Variables ¶
var ApplyStatuses = []ApplyStatus{ApplySucceeded, ApplyFailed, ApplyTimedOut}
ApplyStatuses is the closed set.
var EnvKinds = []EnvKind{EnvPersistent, EnvPreview, EnvSelfManaged, EnvLocal}
EnvKinds is the closed set, in a stable order.
var ErrInvalid = errors.New("invalid release ledger entry")
ErrInvalid is wrapped by every validation failure in this package, so a backend can map "the caller sent a malformed release" to one status code without string-matching.
var ErrReleaseConflict = errors.New("release version already names a different artifact set")
ErrReleaseConflict is returned when a version label is re-cut with a DIFFERENT artifact set. A version that meant two digest sets would void every guarantee promotion rests on, so it is refused rather than resolved.
var FindingClasses = []FindingClass{ClassInfo, ClassWarn, ClassStop}
FindingClasses is the closed set, least to most severe.
var FindingCodes = []string{ FindingObjectAdded, FindingObjectChanged, FindingObjectRemoved, FindingImageChanged, FindingConfigChanged, FindingSecretNeeded, FindingStatefulDeletion, FindingLBIdentityChange, FindingDrift, FindingUnknown, }
FindingCodes is the closed set of codes.
var GateStatuses = []GateStatus{GateStatusPassed, GateStatusFailed, GateStatusSkipped, GateStatusError}
GateStatuses is the closed set, in a stable order (for error messages and for the hosted schema's CHECK, which must list exactly these).
var IdentityKeys = []string{IdentityType, IdentityLoadBalancerIP, IdentityLoadBalancerClass, IdentityStaticIP}
IdentityKeys is the closed set of ShapeObject.Identity keys.
var Kinds = []Kind{KindOCI, KindNPM, KindGoModule, KindFile, KindGit}
Kinds is the closed set, in a stable order (for error messages and for the hosted schema's CHECK, which must list exactly these).
var Modes = []Mode{ModeShared, ModeVariant, ModeSource}
Modes is the closed set.
var ObsCategories = []string{ ObsCategoryBrowser, ObsCategoryLogs, ObsCategoryMetadata, ObsCategoryMetrics, ObsCategoryProfiles, ObsCategoryTraces, }
ObsCategories is every opt-out category, sorted.
var ObsOptIns = []string{ObsOptInReplay}
ObsOptIns is every opt-in category, sorted.
var PlanSections = []string{SectionStateful, SectionLB, SectionObjects, SectionImages, SectionConfig, SectionSecrets, SectionDrift}
PlanSections is the closed set, in display order.
var StageKinds = []StageKind{StageBuild, StageCut, StageCheck, StagePromote, StageRollout}
StageKinds is the closed set, in PIPELINE order: a run's stages sort by time, but this is the order the kinds happen in.
var StageStatuses = []StageStatus{StagePassed, StageFailed, StageRunning, StageSkipped, StageErrored}
StageStatuses is the closed set, in a stable order.
Functions ¶
func BundleClusterPath ¶
BundleClusterPath is the path, within the extracted manifests layer, that holds the documents a deploy routes to one cluster. It is what the control plane sets as `Kustomization.spec.path` for that cluster.
THIS FUNCTION IS THE CONTRACT, AND BOTH SIDES CALL IT. forge writes the tar entries under it and the control plane's Kustomization builder reads it. Two spellings of the same rule would be a silent, total failure: a Kustomization whose path does not exist in the artifact reconciles to "nothing to apply" and reports Ready — so the env would look converged while running the previous release forever. That is why the sanitizer lives here, beside the media type, rather than in whichever package happened to write the tar.
An empty cluster name is the unclustered tree. A multi-cluster env has one path per cluster and ONE bundle: a document routed to two clusters appears under both paths, because each cluster's Kustomization applies its own path and prunes anything it owns that the path no longer carries.
func BundlePathSegment ¶
BundlePathSegment makes one name safe as a single path component: anything that is not alphanumeric, dash, underscore or dot becomes a dash.
A Kubernetes name cannot contain a separator, but a CLUSTER name is a kubectl context and very much can (`gke_project_region_name` is the tame case; a context is free-form). An unsanitized segment would put entries at an unexpected depth, and the unpacker would be the only thing standing between a context name and a path the archive should never ask for.
Exported because the control plane sanitizes the same cluster names when it builds a Kustomization path, and a second implementation that disagreed on one character would point it at a path the artifact does not hold.
func CanonicalRepo ¶
CanonicalRepo normalizes a git remote URL to "host/owner/repo", so the SSH and HTTPS spellings of one repository compare equal and no credential in a URL is ever recorded.
func CheckRecut ¶
CheckRecut decides what re-cutting `existing` as `requested` means: nil when the two name the same bytes (an idempotent retry), and ErrReleaseConflict when they do not. Both backends call this, so "a version label names exactly one artifact set" is one rule.
func ConfigDigest ¶
ConfigDigest is the digest of one env's whole render with every release-bound image reference normalized to its artifact key (doc §2.1).
It is computed from the per-object ConfigHashes a shape already holds, rather than over the manifest bytes, and that is the point: the objects are taken in their canonical (sorted) order, so a render that emits the same objects in a different sequence — or reflows their YAML — produces the same config digest. A digest over the packed layer would move on both, and would therefore report a cosmetic render change as a config change on every deploy.
It covers the object identities as well as their hashes. Renaming an object while leaving its body alone IS a config change, and hashing only the per-object hashes would miss it.
func HashDocument ¶
HashDocument is the object hash: sha256 over the document's CANONICAL JSON, returned as `sha256:<hex>`.
Canonical JSON rather than the rendered YAML, because the hash has to mean "this object" and not "these bytes". encoding/json sorts an object's keys, so a renderer that reorders a map, reflows a list or changes its indentation produces the same hash — while any change to a VALUE changes it. Comparing YAML text would make every cosmetic render change look like drift, which is the signal this hash exists to carry.
It does NOT redact. Callers hashing a rendered document want ObjectHashes; this is exported for the one case that has already redacted and needs the digest of some other canonical value.
func HostID ¶
func HostID() string
HostID is a stable, non-identifying machine id: the first 16 hex of sha256(hostname). Used when no daemon id is available.
func ImagesIn ¶
ImagesIn reports which release artifacts' pinned digests appear in a rendered document, as artifact key → digest. It is what populates ShapeObject.Images.
Searched as TEXT over the whole document, for the reason NormalizeImages is: an image reference is not confined to a container's `image` field. forge's own operators carry the image they launch as an env-var value (control-plane's workspace-controller pins the workspace base image in DAEMON_IMAGE), and a chart's values can put one anywhere. A reader that only looked at pod specs would report such an object as carrying no image while a deploy very much changes which bytes it runs.
func IsNestedCheckout ¶
IsNestedCheckout reports whether dir is the root of a git checkout: a repository (`.git` directory) or a linked worktree (`.git` file), whatever it is named.
func NormalizeImages ¶
NormalizeImages rewrites every release-bound image DIGEST in a document to its artifact key, returning a copy. It is what makes a ConfigHash the identity of a deploy's SHAPE rather than of its images: two renders with equal ConfigHashes differ, at most, in which release they pin, so "promote a new release" and "the KCL moved" are told apart by one comparison instead of by re-rendering and diffing YAML.
pins maps artifact key → pinned digest, as a render resolved them.
ONLY THE DIGEST IS REPLACED, not the whole reference: the repository is part of the config (pushing an image somewhere else IS a change) and the digest is the only part a promotion moves.
Every string in the document is searched, not just a container's `image` field, because an image reference is not confined to one: forge's own operators carry the image they launch as an env-var value, and a chart's values can put one anywhere. A normalizer that only looked at pod specs would leave a digest in the ConfigHash and make every promotion look like a config change.
Artifacts are applied in sorted order, so two artifacts that happen to share a digest normalize the same way every time rather than by map iteration order.
func ObjectHashes ¶
ObjectHashes is the per-object pair a ShapeObject carries, and the only entry point a caller should need.
- hash is the document's digest after secret redaction: what drift detection compares a live object against.
- configHash is the digest of the same document with release-bound image references normalized away: equal configHash with a different hash means only images changed.
It redacts FIRST and unconditionally. A two-call API ("redact, then hash") would make the safe thing the thing a caller has to remember, and the cost of forgetting is a permanent leak.
func RecreatableKind ¶
RecreatableKind reports whether kind may be deleted and re-applied by the immutable-field recovery.
func RedactSecrets ¶
RedactSecrets replaces every value under a `kind: Secret` document's `data` and `stringData` with the hash of that value, returning the document to hash or package.
NO HASH INPUT AND NO PACKAGED BYTE MAY EVER CARRY A SECRET VALUE (F-13). A shape is stored forever and shown to every member of an org, and a bundle is cached and shared, so a leak in either is permanent. This is the one function that makes that true, which is why ObjectHashes calls it rather than trusting its caller to have called it.
THE KEYS SURVIVE. "Which keys does this Secret carry" is a shape fact an operator needs; "what are they" is not. The hash is what keeps "a secret value changed" visible without the value — the signal a What-if diff shows.
It is IDEMPOTENT: a value already carrying the marker is left alone, so redacting a document twice yields the same hash as redacting it once. Re- hashing a marker would make the safe entry point and the correct hash two different calls.
Anything that is not a Secret is returned unchanged, and the input is never mutated — the caller still needs it.
func StatefulKind ¶
StatefulKind reports whether a kind's removal destroys data or everything under it — [statefulKinds] as a predicate.
Exported because the PLAN and the BUNDLE WRITER need the same answer, for two halves of one guarantee. The plan calls a removal of one of these a stop-class finding that must be acknowledged; the bundle writer stamps `kustomize.toolkit.fluxcd.io/prune: disabled` on it so a reconciler cannot delete it at all. Two spellings of this list would mean an object the plan treats as irreplaceable is one the reconciler happily prunes, and the divergence would only ever be discovered by losing the data.
func ValidDigest ¶
ValidDigest reports whether d is a canonical `sha256:<64 hex>` digest.
func ValidObjectID ¶
ValidObjectID reports whether s is a full git object id (SHA-1 or SHA-256).
Types ¶
type Actor ¶
Actor is who recorded a promotion: a human user, or a named automation ("ci", "preview-bot"). A file ledger records whatever the caller states.
type Apply ¶
type Apply struct {
ID string `json:"id"`
Env string `json:"env"`
BundleID string `json:"bundle_id"`
// PromotionID is the binding this apply realizes; "" only for an
// unreleased bundle (a dev-shaped "deploy my worktree").
PromotionID string `json:"promotion_id,omitempty"`
AppliedBy string `json:"applied_by,omitempty"`
Run Run `json:"run,omitempty"`
// PlanDigest is the approved plan this apply realizes (O-13).
PlanDigest string `json:"plan_digest,omitempty"`
// AcknowledgedFindings are the stop-class finding codes the approver
// accepted by name.
AcknowledgedFindings []string `json:"acknowledged_findings,omitempty"`
// SupersededInFlight records that this apply was started over one that
// had not finished — a deliberate override.
SupersededInFlight bool `json:"superseded_in_flight,omitempty"`
// DeadlineAt is forge's wait budget. Past it with no outcome, the apply
// reads as abandoned ([ApplyAbandoned]).
DeadlineAt time.Time `json:"deadline_at"`
ImportedFrom string `json:"imported_from,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
Apply is ONE attempt to make an environment run a bundle — "who tried to make it so, from where" — recorded before anything moves. Its outcome is a SEPARATE record (ApplyOutcome) written at most once, so both halves stay append-only and "the apply never reported back" is visible as the absence of an outcome rather than as a status somebody had to remember to set.
forge writes applies for the envs it applies (self-managed, and the declaration half of a hosted deploy). A converger writes none: a hosted promotion's outcome is DERIVED from observation, and a derived timeline is never also stored.
type ApplyOutcome ¶
type ApplyOutcome struct {
ApplyID string `json:"apply_id"`
Status ApplyStatus `json:"status"`
Summary string `json:"summary,omitempty"`
// Workloads is the applier's per-resource rollout result: one entry per
// resource it waited on.
Workloads []ApplyWorkload `json:"workloads,omitempty"`
// ReportedBy is set by the BACKEND from the credential, never by the
// client.
ReportedBy string `json:"reported_by,omitempty"`
FinishedAt time.Time `json:"finished_at"`
}
ApplyOutcome is the END of an apply, written at most once. It is a REPORT: for a cluster the platform does not observe, the applier is the only observer there is, and a reader must label it as such ("reported by forge"). It never feeds billing or policy.
func (ApplyOutcome) SameReport ¶
func (o ApplyOutcome) SameReport(other ApplyOutcome) bool
SameReport reports whether two outcomes say the same thing — the idempotency rule for a retried report: the same outcome again is a retry, a different one is a conflict. Who reported it and when the retry arrived do not change what was reported.
func (ApplyOutcome) Validate ¶
func (o ApplyOutcome) Validate() error
Validate checks an outcome before it is recorded.
type ApplyState ¶
type ApplyState string
ApplyState is an apply's state as a READER sees it, DERIVED from the apply, its outcome (if any) and the clock. Never stored: "abandoned" in particular is a conclusion about the absence of a report, and a stored conclusion written by a sweeper could be wrong in a way a derivation cannot.
const ( ApplyRunning ApplyState = "running" ApplyStateOK ApplyState = "succeeded" ApplyStateFail ApplyState = "failed" ApplyStateTO ApplyState = "timed_out" // ApplyAbandoned: no outcome arrived by the deadline. "We could not // look" is its own answer — never read as success. ApplyAbandoned ApplyState = "abandoned" )
func DeriveApplyState ¶
func DeriveApplyState(a Apply, outcome *ApplyOutcome, now time.Time) ApplyState
DeriveApplyState is the one derivation every backend and every reader uses.
func (ApplyState) InFlight ¶
func (s ApplyState) InFlight() bool
InFlight reports whether an apply in this state still blocks the next one on the same env. An abandoned apply does not: blocking on a report that will never come would wedge the env.
type ApplyStatus ¶
type ApplyStatus string
ApplyStatus is how an apply ENDED, as its applier reported it. Closed.
const ( ApplySucceeded ApplyStatus = "succeeded" ApplyFailed ApplyStatus = "failed" // ApplyTimedOut: the applier stopped waiting before the rollout // answered. Not a success and not a failure. ApplyTimedOut ApplyStatus = "timed_out" )
func (*ApplyStatus) UnmarshalJSON ¶
func (s *ApplyStatus) UnmarshalJSON(data []byte) error
UnmarshalJSON refuses an empty or unknown status.
func (ApplyStatus) Valid ¶
func (s ApplyStatus) Valid() bool
Valid reports whether s is one of ApplyStatuses.
type ApplyWorkload ¶
type ApplyWorkload struct {
Name string `json:"name"`
Cluster string `json:"cluster,omitempty"`
State string `json:"state"`
Detail string `json:"detail,omitempty"`
}
ApplyWorkload is one resource's rollout result. State is forge's rollout vocabulary: ready | failed | timed_out | not_waited.
type Artifact ¶
type Artifact struct {
Kind Kind `json:"kind"`
Mode Mode `json:"mode"`
// Digests maps a variant key → canonical digest. OCI only: for
// [ModeShared] the only key is [SharedVariant].
Digests map[string]string `json:"digests,omitempty"`
// Platforms is the OS/arch set the image manifest advertises.
// Informational, and NOT part of the artifact's identity.
Platforms []string `json:"platforms,omitempty"`
// Source is set exactly for [ModeSource].
Source *Source `json:"source,omitempty"`
// Version is how a consumer ASKS for a non-OCI artifact.
Version string `json:"version,omitempty"`
// Integrity is how a consumer KNOWS they got the right bytes, in the
// ecosystem's native hash format. Stored verbatim.
Integrity string `json:"integrity,omitempty"`
// URI locates the artifact when name+version does not (a download URL,
// a non-default registry, or the registry an image was pushed to).
URI string `json:"uri,omitempty"`
// BuiltFrom is the git checkout the artifact's build command ran in, when
// that is a checkout forge can name — a ShellBuild's cwd. Informational
// provenance like Platforms, and NOT part of the identity: the digest
// names the bytes, this says where they were built.
BuiltFrom *BuildSource `json:"built_from,omitempty"`
}
Artifact is ONE thing a release froze. Which fields carry the identity depends on Kind and Mode; Artifact.Validate is the rule.
func (Artifact) SharedDigest ¶
SharedDigest returns the digest of a shared OCI artifact. Every other artifact answers ("", false) — that is the guard that keeps packages and source pins out of a pod spec without every caller remembering to check.
type Attest ¶
type Attest struct {
Provider string `json:"provider"`
Subject string `json:"subject"`
RunID string `json:"run_id,omitempty"`
}
Attest is a CI identity claim (an OIDC subject). A hosted backend verifies it before it means anything; a file ledger records it as stated.
type BuildSource ¶
type BuildSource struct {
// Repo is the canonical remote ("github.com/org/repo"); "" when the
// checkout had none.
Repo string `json:"repo,omitempty"`
// Commit is HEAD, full hex.
Commit string `json:"commit"`
// Dirty: the checkout had uncommitted changes when the build started.
Dirty bool `json:"dirty,omitempty"`
}
BuildSource is the checkout an artifact was built in: its repository, HEAD and dirty flag at build time. Like Provenance.Dirty it is a claim the building machine makes, recorded as evidence.
It is distinct from Source, which PINS a source-mode artifact (the commit IS its identity). A BuildSource rides beside an image's digest and explains it; changing it does not change what the release ships.
type BundleClusterTree ¶
type BundleClusterTree struct {
// Cluster is the kubectl context the documents are routed to, or ""
// for the unclustered tree (objects nothing applies).
Cluster string `json:"cluster,omitempty"`
// Path is [BundleClusterPath]'s answer for Cluster: what a
// Kustomization sets as spec.path.
Path string `json:"path"`
// Documents is how many documents the path holds. Carried so a reader
// can tell an empty tree from an absent one without fetching the
// layer, and so a Kustomization is never built over zero documents.
Documents int `json:"documents"`
}
BundleClusterTree is one cluster's documents inside the manifest layer.
type BundleDoc ¶
type BundleDoc struct {
Schema string `json:"schema"`
Project string `json:"project"`
Env string `json:"env"`
// Release is the version the bundle pins, or "" for an unreleased
// bundle (dev, or a Preview deploy) pinned to a build's own digests.
Release string `json:"release"`
// ConfigDigest is the digest of the render with every release-bound
// image reference normalized to its artifact key: the shape of the
// deploy independent of which release it pins. Two bundles with equal
// ConfigDigests differ, at most, in images.
ConfigDigest string `json:"config_digest"`
Pins BundlePins `json:"pins"`
Provenance Provenance `json:"provenance"`
Shape Shape `json:"shape"`
// ClusterPaths is one entry per path the manifest layer actually
// carries: the cluster, and the path under which its documents live.
//
// THE BUNDLE DESCRIBES ITS OWN LAYOUT so the reconciler's operator
// builds one Kustomization per cluster without re-deriving anything.
// It could compute the paths from Shape.Clusters and
// [BundleClusterPath], and that is exactly the coupling this removes:
// Shape.Clusters is the env's DECLARED cluster list, while these are
// the paths the layer HOLDS, and the two differ whenever a declared
// cluster ends up with no documents routed to it. A Kustomization
// built for the declared-but-empty cluster points at a path that does
// not exist, which reconciles green over an env that was never
// applied.
//
// So it is written from the tar entries themselves, after they are
// written, and a reader can trust it the way it trusts the digest.
ClusterPaths []BundleClusterTree `json:"cluster_paths,omitempty"`
// Secrets names the Secret objects the env requires in each cluster.
// NAMES ONLY: a Secret never rides the manifest layer, not even
// redacted, so the reconciler neither writes nor prunes one. `forge env
// deploy` syncs them to the cluster itself, before pointing the
// reconciler at this bundle; a hub-reconciled env reads this list to
// know what its clusters must already hold.
Secrets []BundleSecretRef `json:"secrets,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
BundleDoc is the config blob of a bundle: what one env's render, from one checkout, pinned to one release, IS — without its manifests. It lives inside the content-addressed bundle, so the bundle digest covers it, and a backend that records a bundle reads the shape and provenance out of these verified bytes rather than trusting a request field that describes them.
func DecodeBundleDoc ¶
DecodeBundleDoc strictly decodes a bundle config blob: unknown fields and a foreign schema are refused rather than ignored, because a backend records what it decoded and must not record a summary of a document it only half understood.
type BundlePins ¶
type BundlePins struct {
Images map[string]string `json:"images,omitempty"`
Sources map[string]Source `json:"sources,omitempty"`
}
BundlePins is the pin set a bundle was rendered with: the same two halves a promotion freezes.
type BundleRecord ¶
type BundleRecord struct {
ID string `json:"id"`
Env string `json:"env"`
// Release is the version pinned, "" for an unreleased bundle.
Release string `json:"release,omitempty"`
// Digest is the OCI MANIFEST digest: the bundle's identity, and the
// revision a reconciler reports.
//
// THIS IS THE VALUE THE CONTROL PLANE WRITES TO THE HUB OCIRepository'S
// `spec.ref.digest`, and therefore the value a convergence read compares
// `Kustomization.status.lastAppliedRevision` against. Measured
// read-only on the dev hub, the chain is one unchanged string:
//
// spec.ref.digest == status.artifact.revision == lastAppliedRevision
// sha256:<64 hex> sha256:<64 hex> sha256:<64 hex>
//
// and this field is produced in exactly that form ([ValidDigest]
// enforces it). So the comparison is a plain string equality, which is
// what control-plane's fluxobserve.judge performs — no normalization,
// no tag prefix, nothing to strip on either side.
//
// It is the MANIFEST digest rather than the manifests LAYER's, and the
// distinction is the one thing to get right when the hub moves from
// per-deployment config artifacts to `bundle.v1/<env>`: a bundle's
// identity, what a promotion names, and what Flux resolves `ref.digest`
// to are all this one value, while the layer digest is an internal
// detail of the artifact that nothing outside the bundle addresses.
// Selecting a layer with `layerSelector` changes which bytes land in the
// artifact, not which revision the source reports.
//
// That last sentence is reasoned from Flux's model (the revision is the
// resolved manifest digest, fixed before layer extraction) and NOT
// measured: no OCIRepository on the dev hub sets a layerSelector today,
// and confirming it needs a cluster that does. It is worth one assertion
// in the first integration test that points a Kustomization at a bundle
// — if it were ever false, every env would read as permanently
// progressing while being perfectly converged.
Digest string `json:"digest"`
// Reference is repo@digest, or an OCI-layout reference for a file
// ledger.
Reference string `json:"reference"`
ConfigDigest string `json:"config_digest"`
Shape Shape `json:"shape"`
Provenance Provenance `json:"provenance"`
Run Run `json:"run,omitempty"`
CreatedBy string `json:"created_by,omitempty"`
ImportedFrom string `json:"imported_from,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
BundleRecord is a bundle as a LEDGER holds it: the document's indexable half plus where the bytes are and who recorded them. Immutable.
type BundleSecretRef ¶
type BundleSecretRef struct {
// Cluster is the kubectl context the Secret belongs in, or "" when the
// render attributed it to no cluster.
Cluster string `json:"cluster,omitempty"`
Namespace string `json:"namespace,omitempty"`
Name string `json:"name"`
}
BundleSecretRef is one Secret the env requires: where it lives and what it is called. It carries no value and no key names.
type CaptureOptions ¶
type CaptureOptions struct {
// ForgeVersion is the rendering binary's version.
ForgeVersion string
// WorktreeKey is forge's devstack key for the checkout ("" = primary).
WorktreeKey string
// Host names the machine (a daemon id, or HostID()). Never a raw
// hostname.
Host string
// Attestation is the CI identity, when there is one.
Attestation *Attest
}
CaptureOptions are the facts CaptureProvenance cannot learn from git.
type Changes ¶
type Changes struct {
// Paths are the checkout's own uncommitted changes — tracked edits,
// deletions, and untracked-unignored files — relative to the top of the
// checkout, in git's order.
Paths []string
// Nested are untracked directories that are the root of ANOTHER git
// checkout (a parked worktree, a cloned repo), with git's trailing slash.
// They are not this checkout's content and are not in Paths.
Nested []string
}
Changes is what `git status` reports for a checkout, split by whether it is this checkout's own content.
func InspectCheckout ¶
InspectCheckout reads `git status` for the checkout whose top is top.
A NESTED GIT CHECKOUT IS NOT DIRT. An untracked directory with its own .git is another repository (agents park linked worktrees inside the project directory), and the status of the repository around it says nothing about the bytes this checkout would build. Counting it made a clean checkout "dirty", which cut and built a whole new release for an unchanged tree. Every other untracked file still counts.
func InspectDir ¶
InspectDir is InspectCheckout for any directory inside a checkout. A directory that is not in a git checkout has no changes and is not an error, as in CaptureProvenance.
type DriftObservation ¶
type DriftObservation struct {
Drifted []ObjectKey
}
DriftObservation is what an observer saw of the live cluster.
type EnvKind ¶
type EnvKind string
EnvKind is how an environment runs, and therefore who applies it and whether its secrets can be read back. Closed. It is a PREDICATE over an env's rendered declaration — forge derives it, nobody selects it — and a backend treats it as immutable once recorded: an env whose kind changed is a different env.
const ( // EnvPersistent: the platform runs it (something is bound OnHosted). // A converger applies its promotions; its secrets are write-only. EnvPersistent EnvKind = "persistent" // EnvPreview: a persistent env with a TTL. EnvPreview EnvKind = "preview" // EnvSelfManaged: a control plane keeps its ledger, nothing is hosted, // and something runs on a cluster the author operates. forge applies // it; the platform places nothing. Its secrets are write-only, which is // the point of separating it from EnvLocal: a cluster env's secrets must // never be pullable. EnvSelfManaged EnvKind = "self_managed" // EnvLocal: every workload runs on a developer machine (`forge env // up`). Never a deploy target; its secrets are readable back. EnvLocal EnvKind = "local" )
func ParseEnvKind ¶
ParseEnvKind reads a stored or wire kind, refusing anything outside the set.
func (EnvKind) Placed ¶
Placed reports whether the platform holds placement (a namespace on its fleet) for an env of this kind, and so converges and observes it.
func (EnvKind) Promotable ¶
Promotable reports whether an env of this kind can be bound to a release. Everything but LOCAL: a self-managed env IS promoted; it is just applied by forge rather than converged by the platform.
func (*EnvKind) UnmarshalJSON ¶
UnmarshalJSON refuses an empty or unknown kind.
type EnvRecord ¶
type EnvRecord struct {
Name string `json:"name"`
Kind EnvKind `json:"kind"`
DeclaredShape *Shape `json:"declared_shape,omitempty"`
DeclaredBy *Provenance `json:"declared_by,omitempty"`
DeclaredAt *time.Time `json:"declared_at,omitempty"`
}
EnvRecord is an environment as a ledger holds it: its identity, its kind, and the most recently DECLARED shape — what its KCL said the last time someone built it. DeclaredShape is what lets a reader answer "what kind, which secrets, which provider" without rendering anything, which is what makes the Live view work with no daemon.
type FindingClass ¶
type FindingClass string
FindingClass is how much a finding gates a deploy. Closed.
const ( // ClassInfo: a change the operator should see. Approval covers it. ClassInfo FindingClass = "info" // ClassWarn: something to read before approving — a missing secret, // drift the deploy will overwrite, a section that could not be // computed. Approval covers it. ClassWarn FindingClass = "warn" // ClassStop: a change a general approval must NOT cover — a stateful // deletion, a load balancer's identity changing. It needs its own // acknowledgement BY CODE, even with --yes, because --yes is the flag // that ends up hard-coded in CI and would otherwise pre-approve every // future destructive change. ClassStop FindingClass = "stop" )
func (*FindingClass) UnmarshalJSON ¶
func (c *FindingClass) UnmarshalJSON(data []byte) error
UnmarshalJSON refuses an unknown class.
func (FindingClass) Valid ¶
func (c FindingClass) Valid() bool
Valid reports whether c is one of FindingClasses.
type Gate ¶
type Gate struct {
// Name is the check: "lint", "test", "smoke", "rollout", "qa-signoff".
Name string `json:"name"`
// Status is the verdict, from the closed set.
Status GateStatus `json:"status"`
// RawStatus is the stored status when it was NOT in the closed set —
// free text from a ledger written before the set closed. Empty on
// every gate whose status is recognised, so a non-empty value is
// exactly the signal "this entry predates the vocabulary".
// Set only by the READ path; never sent.
RawStatus string `json:"raw_status,omitempty"`
// URL is where the full report lives (a CI run, an artifact).
URL string `json:"url,omitempty"`
// Summary is one line: "412 passed, 0 failed, 3 skipped".
Summary string `json:"summary,omitempty"`
// StartedAt and FinishedAt are the check's own window, not when it was
// recorded. They are what makes a gate a run stage with a duration.
StartedAt *time.Time `json:"started_at,omitempty"`
FinishedAt *time.Time `json:"finished_at,omitempty"`
// RunID ties this check to one [Run]. Optional: a manual sign-off
// belongs to a promotion and to no run.
RunID string `json:"run_id,omitempty"`
// Details is small, structured and optional — counts and verdicts, not
// logs. A hosted backend caps it (8 KiB); the full report is behind
// URL.
Details map[string]any `json:"details,omitempty"`
// RecordedBy and RecordedAt are SET BY THE BACKEND that recorded the
// gate, from the authenticated principal — a user id, or
// "token:<token-id>". Never accepted from a request, because evidence
// whose author the author chose is not attributable. A file ledger
// leaves them empty: it has no principal to attest to.
RecordedBy string `json:"recorded_by,omitempty"`
RecordedAt *time.Time `json:"recorded_at,omitempty"`
}
Gate is one check's result, attached to a promotion. EVIDENCE, not enforcement: a gate that must block a promotion is checked BEFORE the entry is written, because a recorded claim proves only that it was made, and by whom.
A gate carries timing, so a gate IS a run stage (see Stage): everything a run does that is not derivable from the ledger — build, lint, test, smoke, wait — is recorded as one of these.
func (*Gate) UnmarshalJSON ¶
UnmarshalJSON is the LENIENT read path: a status outside the closed set becomes GateStatusError with the original kept on RawStatus, so a promotion recorded before the vocabulary existed still reads back. Use Gate.Validate to refuse writing one.
type GateStatus ¶
type GateStatus string
GateStatus is the verdict a check reported. CLOSED: see the package doc.
There are two parse paths on purpose, because a status arrives from two very different places:
- ParseGateStatus is the WRITE path — a --gate flag, an RPC request. It refuses anything outside the set, so a typo fails before the entry is recorded.
- GateStatusFromStored is the READ path — a ledger line, a database row. Entries written before the set was closed carry free text, and refusing to read them would make old promotions unrenderable. They map to GateStatusError and the raw value is kept verbatim on Gate.RawStatus.
const ( // GateStatusPassed: the check ran and found nothing wrong. GateStatusPassed GateStatus = "passed" // GateStatusFailed: the check ran and found something wrong. This is a // VERDICT, not an error — the check itself worked. GateStatusFailed GateStatus = "failed" // GateStatusSkipped: the check did not apply. A hosted smoke with no // URL-bearing workload is skipped, never passed: a green check that // checked nothing is the failure mode this value exists to prevent. GateStatusSkipped GateStatus = "skipped" // GateStatusError: the check could not reach a verdict — it crashed, // timed out, or could not look. Distinct from failed for the same // reason exit code 2 is distinct from exit code 1. GateStatusError GateStatus = "error" )
func GateStatusFromStored ¶
func GateStatusFromStored(stored string) (status GateStatus, raw string)
GateStatusFromStored reads a STORED status. It returns the status to display and, when the stored value was NOT in the closed set, that value verbatim so nothing is lost. An unrecognised status displays as GateStatusError: a claim nobody can interpret must not render as a pass.
An EMPTY stored status is not mapped. Absent is absent, not an error verdict: a gate that recorded no status made no claim, and inventing one for it would turn a hole in the data into a reported outcome. It comes back empty, which Gate.Validate refuses — the same rule as a missing artifact kind.
func ParseGateStatus ¶
func ParseGateStatus(s string) (GateStatus, error)
ParseGateStatus reads a status a CALLER supplied — a `--gate` flag, an RPC request field — and refuses anything outside the closed set. Empty is refused too: a check that reported no verdict has not reported.
func (*GateStatus) UnmarshalJSON ¶
func (s *GateStatus) UnmarshalJSON(data []byte) error
UnmarshalJSON decodes through ParseGateStatus. It is STRICT, because a bare GateStatus is only decoded where a caller supplied one; a stored entry is decoded through Gate.UnmarshalJSON, which is lenient.
func (GateStatus) Valid ¶
func (s GateStatus) Valid() bool
Valid reports whether s is one of GateStatuses.
type Git ¶
type Git struct {
Commit string `json:"commit,omitempty"`
Tag string `json:"tag,omitempty"`
// Dirty means the release was cut from a tree with uncommitted changes:
// its bytes correspond to no reviewable commit.
Dirty bool `json:"dirty,omitempty"`
}
Git is a release's source provenance. Best-effort: empty on a non-git tree.
type ImageChange ¶
type ImageChange struct {
Artifact string `json:"artifact"`
From string `json:"from,omitempty"`
To string `json:"to,omitempty"`
}
ImageChange is one artifact's digest moving.
type Kind ¶
type Kind string
Kind says WHAT an artifact is, and therefore how it is addressed and verified. Closed: see the package doc.
const ( // KindOCI is a container image, addressed by digest alone. KindOCI Kind = "oci" // KindNPM is an npm package, addressed by version + integrity. KindNPM Kind = "npm" // KindGoModule is a Go module, addressed by version + go.sum hash. KindGoModule Kind = "gomod" // KindFile is a published file, addressed by version/URI + content hash. KindFile Kind = "file" // KindGit is source built at deploy time (a forge.GitSource frontend), // addressed by the commit its declared ref resolved to. KindGit Kind = "git" )
func (*Kind) UnmarshalJSON ¶
UnmarshalJSON refuses an empty or unknown kind.
type KindChange ¶
KindChange is a disagreement about an env's kind.
type LocalSession ¶
type LocalSession struct {
// ID is minted by forge and stable across heartbeats.
ID string `json:"id"`
Env string `json:"env"`
Worktree Worktree `json:"worktree"`
Provenance Provenance `json:"provenance"`
// BundleDigest names the LOCAL bundle the stack runs; the blob never
// leaves the machine.
BundleDigest string `json:"bundle_digest,omitempty"`
StartedAt time.Time `json:"started_at"`
LastSeenAt time.Time `json:"last_seen_at"`
StoppedAt *time.Time `json:"stopped_at,omitempty"`
}
LocalSession is one `forge env up` stack, on one machine, in one checkout: PRESENCE, not ledger. It names no promotion and no release, is mutable (heartbeats), is garbage-collected, and nothing authorizes or bills on it. It exists so Live can show what is running where without asking a daemon.
func (LocalSession) Live ¶
func (s LocalSession) Live() bool
Live reports whether the session has not been stopped.
func (LocalSession) Stale ¶
func (s LocalSession) Stale(now time.Time) bool
Stale reports whether a live session has gone quiet.
func (LocalSession) Validate ¶
func (s LocalSession) Validate() error
Validate checks a session report.
type Mode ¶
type Mode string
Mode says how an artifact's identity is PINNED — a separate axis from Kind. Closed.
const ( // every environment. OCI digests live under [SharedVariant]. ModeShared Mode = "shared" // ModeVariant: genuinely different per-environment builds (a // NEXT_PUBLIC_* baked at build time). Digests is keyed by variant name. ModeVariant Mode = "variant" // ModeSource: built from a pinned commit at deploy time; the identity // is [Artifact.Source]. Only [KindGit] uses it. ModeSource Mode = "source" )
func (*Mode) UnmarshalJSON ¶
UnmarshalJSON refuses an empty or unknown mode.
type ObjectChange ¶
type ObjectChange struct {
Live ShapeObject `json:"live"`
Candidate ShapeObject `json:"candidate"`
// ConfigChanged: the non-image half of the object changed. When either
// side lacks a ConfigHash the split cannot be made, and ConfigChanged
// is true whenever the hashes differ (the conservative reading).
ConfigChanged bool `json:"config_changed"`
// Images are the release-bound images whose pinned digest changed.
Images []ImageChange `json:"images,omitempty"`
}
ObjectChange is one object present on both sides whose hash differs.
type ObjectKey ¶
type ObjectKey struct {
Cluster string `json:"cluster"`
Kind string `json:"kind"`
Namespace string `json:"namespace,omitempty"`
Name string `json:"name"`
}
ObjectKey is an object's identity within a shape: (cluster, kind, namespace, name). The API version is deliberately not part of it — moving a Deployment from apps/v1beta2 to apps/v1 is a change to the same object.
type Plan ¶
type Plan struct {
Digest string `json:"digest"`
EnvironmentID string `json:"environment_id"`
BundleID string `json:"bundle_id"`
ReleaseVersion string `json:"release_version,omitempty"`
LiveBasis PlanBasis `json:"live_basis"`
Findings []PlanFinding `json:"findings"`
// ConfigIdentical: the bundle's config digest equals the live one, so
// the deploy changes images at most. A no-op deploy says so instead of
// manufacturing a diff to approve.
ConfigIdentical bool `json:"config_identical"`
ComputedAt time.Time `json:"computed_at"`
}
Plan is one reviewed deploy. Field names match controlplane.v1.DeployPlan.
func BuildPlan ¶
BuildPlan computes the plan. Pure and deterministic: the same input yields the same findings in the same order and the same digest. ComputedAt is the only clock-dependent field and is left for the caller to set.
func (Plan) Unacknowledged ¶
Unacknowledged returns the stop-class codes the acknowledgements do not name. Empty means the plan may be approved. Acknowledgement is by CODE, so one acknowledgement covers every finding of that code in THIS plan — and, because the digest binds the approval to this plan, nothing beyond it.
func (Plan) VerifyDigest ¶
VerifyDigest recomputes the digest and reports whether it matches Digest.
type PlanBasis ¶
type PlanBasis struct {
CurrentPromotionID string `json:"current_promotion_id,omitempty"`
AppliedBundleID string `json:"applied_bundle_id,omitempty"`
AppliedConfigDigest string `json:"applied_config_digest,omitempty"`
DriftObserved bool `json:"drift_observed"`
}
PlanBasis is what a plan was computed AGAINST. Any change to it — another promotion, another bundle applied, drift appearing — changes the digest, so an approval of a plan computed against an older Live is refused as stale.
type PlanFinding ¶
type PlanFinding struct {
Code string `json:"code"`
Class FindingClass `json:"class"`
Section string `json:"section"`
// Subject is "<cluster>/<kind>/<ns>/<name>" for an object, else the
// workload, artifact or secret name, or the section for an unknown.
Subject string `json:"subject"`
// Detail is human-readable and NEVER a secret value. Not digested.
Detail string `json:"detail,omitempty"`
}
PlanFinding is one thing the plan says.
type PlanInput ¶
type PlanInput struct {
EnvironmentID string
BundleID string
ReleaseVersion string
Candidate Shape
CandidateConfigDigest string
// Live is the applied bundle's shape, else the env's declared shape;
// nil when nothing is recorded (every object then reads as added, and
// the plan carries an unknown finding).
Live *Shape
// Basis is the live state the plan is bound to. AppliedConfigDigest is
// also what ConfigIdentical compares against.
Basis PlanBasis
// Drift is the live-cluster observation: nil when drift is not
// observable for this env (an unknown finding), else the objects found
// diverged from the applied bundle (possibly none).
Drift *DriftObservation
// SecretPresence maps a secret name to whether its value is set, for
// the secrets whose presence the caller COULD read: a managed store, or
// the target cluster's Secrets when the caller is the one applying to
// it. A name the map does not contain is UNVERIFIABLE — a store or a
// cluster that could not be read — never "missing": one env commonly
// mixes providers, and reading every unread secret as missing would make
// the warning meaningless. nil means nothing could be read.
//
// It is consulted for every declared secret: a newly declared one is a
// finding either way (info when present), and a previously declared one
// is a finding only when explicitly false — declared at the last apply,
// missing now.
SecretPresence map[string]bool
}
PlanInput is everything BuildPlan reads. It is assembled by the caller from records it already holds — the candidate bundle and the live state — so BuildPlan itself does no I/O.
type Promotion ¶
type Promotion struct {
// ID is assigned by the backend that recorded the entry.
ID string `json:"id,omitempty"`
// Env is the environment name.
Env string `json:"env"`
// Release is the version label bound.
Release string `json:"release"`
Kind PromotionKind `json:"kind"`
// FromEnv is the environment this release was promoted FROM, or empty
// for a first deploy or a direct promote. Makes the promotion PATH auditable.
FromEnv string `json:"from_env,omitempty"`
// Resolved is the image → digest pin set FROZEN at promote time. A
// deploy pins exactly these; nothing re-reads the release later.
Resolved map[string]string `json:"resolved"`
// Sources is the source-built half of the same snapshot.
Sources map[string]Source `json:"sources,omitempty"`
PromotedBy Actor `json:"promoted_by,omitempty"`
// Gates is the PRE-promote evidence, frozen into the entry: what had
// already passed when the environment was bound.
Gates []Gate `json:"gates,omitempty"`
// RecordedGates is the POST-promote evidence — the wait, the smoke, a
// manual sign-off — appended after the binding. It is a separate field
// because the promotion entry is append-only: evidence that arrives
// later cannot be written into Gates without rewriting history, and
// the distinction is itself meaningful (what was known BEFORE the
// environment moved, versus what was learned after).
//
// A file ledger leaves it empty: there is nowhere to append to a line
// already written. A hosted backend fills it from its child table.
RecordedGates []Gate `json:"recorded_gates,omitempty"`
// Run is the pipeline attempt that made this promotion, or the zero
// Run for a promotion that belongs to none.
Run Run `json:"run,omitempty"`
// FromPromotionID is the SOURCE promotion this one copied pins from —
// the exact entry, not just the environment. FromEnv names where the
// bytes came from; this names WHEN, which is what makes a promote
// chain reconstructible after the source env has moved on.
FromPromotionID string `json:"from_promotion_id,omitempty"`
// SupersededInFlight records that this promotion replaced one whose
// rollout had not finished — a deliberate override, not an accident.
// Set by the backend that admitted the override; it is the audit trail
// for "who decided to interrupt a rollout".
SupersededInFlight bool `json:"superseded_in_flight,omitempty"`
// Note is the free-form "why" — the most valuable field on a promote
// that moves an environment backwards.
Note string `json:"note,omitempty"`
// PlanDigest is the approved deploy plan this promotion was written
// under ([Plan.Digest]). Writing the promotion IS the deploy wherever a
// converger applies it, so the review precedes the record, and this
// names which review.
PlanDigest string `json:"plan_digest,omitempty"`
// ApprovedBy is who approved that plan. A backend sets it from the
// credential; a client never supplies it as identity.
ApprovedBy string `json:"approved_by,omitempty"`
// AcknowledgedFindings are the stop-class finding codes the approver
// accepted by name.
AcknowledgedFindings []string `json:"acknowledged_findings,omitempty"`
// PromotedAt is when the entry was written. It is PROMOTE time, not
// deploy time: a promotion moves no bytes.
PromotedAt time.Time `json:"promoted_at"`
}
Promotion is ONE append-only ledger entry: environment Env ran release Release from At onward. An environment's CURRENT binding is simply its most recent entry; there is no separate pointer that could disagree with the history.
func Decide ¶
Decide applies the ledger's append rule to one environment's history (OLDEST FIRST) and a requested entry. It returns:
- (current, nil) when the request is already the current state — the same release. A CI retry must not append a second entry claiming the environment moved where it already was. The key is the CURRENT entry, not "ever promoted": v1→v2→v1 is three real moves, and the third must be recorded.
- (nil, nil) when the entry should be appended.
Both backends call this inside whatever serializes their appends (the hosted one under a row lock), so the rule has one implementation. The error return is kept so a future rule can refuse without a signature change across both backends.
func NewPromotion ¶
func NewPromotion(env string, r Release, kind PromotionKind) Promotion
NewPromotion freezes a release's pin set into a promotion of env. The digests are resolved HERE, at promote time, so a later edit to the release cannot change what this promotion ships.
type PromotionKind ¶
type PromotionKind string
PromotionKind records what a ledger entry is. There is ONE kind: a promotion binds an environment to a release. Moving an environment to an OLDER release is still a promotion — the plan labels its direction `behind` — because there is no such thing as undoing a release: the newer one already wrote data and ran migrations. Recovery is roll forward.
The field survives because it is the ledger's wire format, and an explicit kind keeps a future entry type from being read as a promotion.
const ( // KindPromote binds an environment to a release. KindPromote PromotionKind = "promote" )
func ParsePromotionKind ¶
func ParsePromotionKind(stored string) (PromotionKind, error)
ParsePromotionKind reads a STORED kind — a ledger line, a database row — into the kind it means today. It is the one place the retired "rollback" kind is recognised, so every backend that holds old entries reads them the same way: as the promote of their release. Unknown and empty are refused, never defaulted — a kind nobody recognises must not read as a binding.
func (*PromotionKind) UnmarshalJSON ¶
func (k *PromotionKind) UnmarshalJSON(data []byte) error
UnmarshalJSON decodes through ParsePromotionKind.
func (PromotionKind) Valid ¶
func (k PromotionKind) Valid() bool
Valid reports whether k is a kind a ledger entry may carry.
type Provenance ¶
type Provenance struct {
// Repo is the canonical remote, e.g. "github.com/reliant-labs/forge".
// Empty when the checkout has no remote.
Repo string `json:"repo"`
// Commit is HEAD, full hex. Empty when the source is not a git tree.
Commit string `json:"commit"`
// Branch is the symbolic ref at build time; empty on a detached HEAD.
Branch string `json:"branch,omitempty"`
// Tag is an exact tag at HEAD, if any.
Tag string `json:"tag,omitempty"`
// Dirty: tracked OR untracked-unignored changes existed at build time.
Dirty bool `json:"dirty"`
// Tree is the git tree hash of the BUILT content, dirty changes
// included, computed through a temporary index (the user's index is
// never touched). Empty means it was not hashed — no git, or the
// working tree was too large to hash within the capture budget. A
// clean checkout's Tree equals HEAD^{tree}.
Tree string `json:"tree"`
// Worktree identifies the checkout on its machine.
Worktree Worktree `json:"worktree"`
// ForgeVersion is the binary that rendered. A render is a function of
// (forge, KCL, config), so the same commit rendered by two forges is
// two different artifacts.
ForgeVersion string `json:"forge_version"`
// Attestation is the CI identity that built it, or nil from a laptop.
Attestation *Attest `json:"attestation,omitempty"`
}
Provenance is WHERE a release or a bundle came from: the source tree, the checkout it was built in, and the forge that rendered it. It is shared by releases (the images) and bundles (the config), so "images from main@abc, config from feat-x@def, dirty" is two values of one type.
Claims and checkable facts ¶
What a CLIENT may claim and what a SERVER can establish are kept apart, and every reader must keep them apart too:
- Commit is checkable against the repository.
- Tree is checkable when Dirty is false: a clean tree's hash IS the commit's tree, so a clean claim can be verified without trusting the client.
- Branch, Worktree and Dirty are claims. They are evidence and are rendered as such ("reported by forge"), never used as authority.
- Ancestry ("is this commit on main") is never a field here. It changes over time — a branch is merged, a force-push orphans a commit — so it cannot sit on an immutable record. A backend derives it.
Release.Git predates this type and is kept, filled from it (see Release.SetProvenance), so ledgers written before Provenance existed still decode and readers that only know Git keep working.
func CaptureProvenance ¶
func CaptureProvenance(ctx context.Context, dir string, opts CaptureOptions) (Provenance, error)
CaptureProvenance records where the content in dir came from. It is best-effort in exactly one way: a directory that is not a git checkout yields a Provenance with no commit (and an error only when git itself is broken in a way the caller should hear about). It NEVER touches the user's index: the tree hash is computed through a temporary one.
Exported so the daemon and CI capture provenance with the same code forge does — the field set is ledger vocabulary every backend must agree on.
func (Provenance) ForHosted ¶
func (p Provenance) ForHosted() Provenance
ForHosted is the copy that may leave this machine: the worktree path is dropped. Everything else is already path-free by construction.
func (Provenance) Git ¶
func (p Provenance) Git() Git
Git is the legacy three-field view of this provenance.
func (Provenance) Unhashed ¶
func (p Provenance) Unhashed() bool
Unhashed reports whether this provenance names a commit but could not hash what was built (F-16). Such a record is honest about the gap rather than pretending the tree is HEAD's.
func (Provenance) Validate ¶
func (p Provenance) Validate() error
Validate checks the shape of the claims. It cannot check their truth.
type Release ¶
type Release struct {
// Version is the label ("v1.4.0"). Releases are ADDRESSED by it, in
// every backend.
Version string `json:"release"`
// Project is the forge project (forge.yaml `name`) the version belongs
// to. A version label is unique within a project, not across an org:
// two projects may both cut v1.0.0. "" is a release recorded before
// releases carried a project.
Project string `json:"project,omitempty"`
// Git is the legacy provenance view, kept filled from Provenance (see
// SetProvenance) so readers that predate Provenance keep working.
Git Git `json:"git"`
// Provenance is where the release's images were built from. Nil on a
// release recorded before it existed.
Provenance *Provenance `json:"provenance,omitempty"`
// CreatedAt is when the release was cut.
CreatedAt time.Time `json:"created_at"`
// CreatedBy is who cut it, when the backend knows (a hosted ledger
// does; a file does not).
CreatedBy string `json:"created_by,omitempty"`
// Artifacts maps the bare artifact name (an image name, a package name,
// a frontend name) → its identity.
Artifacts map[string]Artifact `json:"artifacts"`
// Run is the pipeline attempt that cut the release, or the zero Run
// when a human cut it. It is NOT part of the release's identity:
// [Release.SameContent] ignores it, so a CI retry that re-cuts the
// same bytes under a new run id is still an idempotent re-cut.
Run Run `json:"run,omitempty"`
}
Release is an immutable, content-addressed set of artifacts under a version label.
func (Release) ArtifactNames ¶
ArtifactNames returns the artifact names, sorted.
func (Release) SameContent ¶
SameContent reports whether two releases name the same bytes: the same artifact set with the same identities. Provenance and timestamps are not compared — a re-cut from CI's retry carries a fresh CreatedAt and is still the same release.
func (*Release) SetProvenance ¶
func (r *Release) SetProvenance(p Provenance)
SetProvenance records where the release was built from, and keeps the legacy Git view in step with it. A release is shared — promoted between envs, imported, shown to every member — so it never carries the build machine's checkout path (Provenance.ForHosted).
func (Release) SharedDigests ¶
SharedDigests is the image-name → digest pin set a promotion freezes: every shared OCI artifact, and nothing else.
type Run ¶
type Run struct {
// ID is opaque and CALLER-chosen, stable across retries of one
// attempt: "github:acme/app/12345/1". Stable-across-retries is the
// property that makes gate recording idempotent on
// (promotion, name, run id) — a re-run of one CI step must not
// double-record.
ID string `json:"id"`
// URL links to the run in whatever ran it.
URL string `json:"url,omitempty"`
// Provider is "github", "gitlab", "manual". DISPLAY ONLY — nothing
// branches on it, and it is not part of the run's identity. A verb
// that behaved differently per provider is the thing this field exists
// to avoid needing.
Provider string `json:"provider,omitempty"`
}
Run is the identity of ONE pipeline attempt, carried by everything that attempt wrote: the release it cut, the promotions it made, the gates it recorded. It is what turns a scatter of ledger rows into a timeline somebody can read.
It is deliberately thin. A run is not stored as an entity anywhere: there is no runs table and no run events, because every stage already leaves a durable, timestamped fact in the ledger (a release's CreatedAt, a promotion's PromotedAt, a gate's window). The ID is the join key over those facts, and nothing more. A parallel event log would be a second source of truth free to disagree with the first.
type RunTimeline ¶
type RunTimeline struct {
Run Run `json:"run"`
// Release is the version label the run concerns, empty when unknown.
Release string `json:"release,omitempty"`
// Stages are ordered by StartedAt, oldest first.
Stages []Stage `json:"stages"`
}
RunTimeline is one run, assembled. The release is the one a run cut, or the one its promotions bound when the run cut nothing.
func (RunTimeline) Verdict ¶
func (t RunTimeline) Verdict() StageStatus
Verdict collapses a timeline into one answer, which is what a final "did this run pass" pipeline step needs. The order is deliberate and is NOT worst-wins alphabetically:
failed > errored > running > passed
A failure outranks a still-running stage because a run with a failed stage has failed whatever the rest does. Running outranks passed because a run that has not finished has not passed. A run with no stages at all is StageRunning: an id nothing has written under is not a pass.
type RuntimeChange ¶
type RuntimeChange struct {
Workload string `json:"workload"`
From string `json:"from"`
To string `json:"to"`
FromCluster string `json:"from_cluster,omitempty"`
ToCluster string `json:"to_cluster,omitempty"`
}
RuntimeChange is a workload moving between runtimes ("web: hosted → bucket") or clusters.
type Shape ¶
type Shape struct {
Kind EnvKind `json:"kind"`
Workloads []ShapeWorkload `json:"workloads"`
Secrets []ShapeSecret `json:"secrets"`
Domains []string `json:"domains"`
Clusters []string `json:"clusters"`
Objects []ShapeObject `json:"objects"`
// ControlPlaneSecrets are the Secrets the env's declared control plane
// supplies and renews (forge.ControlPlaneSecret): where each lands and what
// to mint into it. It is how the control plane learns what to deliver, so
// it is recorded on every ensure. Omitted when empty, so an env that
// declares none encodes, and hashes, exactly as it did before the field.
ControlPlaneSecrets []ShapeControlPlaneSecret `json:"control_plane_secrets,omitempty"`
// Alerts are the env's resolved alert rules: its own (forge.AlertRule) plus
// the generated defaults it did not disable, each tagged with its source.
// The control plane evaluates them; a rollback restores the rules of the
// release it returns to. Each entry serializes to pkg/alerting's RuleSpec.
// Omitted when empty, like ControlPlaneSecrets.
Alerts []ShapeAlert `json:"alerts,omitempty"`
// AlertRoutes say which receiver an alert goes to, by label, first match
// wins unless a route continues. Receivers are NAMES: their secrets live
// in the control plane. Order is significant and is preserved.
AlertRoutes []ShapeAlertRoute `json:"alert_routes,omitempty"`
// ObservabilityPolicy is the env's resolved observability opt-out
// (forge.Observability): what the author switched off. It is how the
// platform learns what NOT to collect, enforced at the source (the node
// agent, the operator's injection, the browser ingest). Omitted when the
// env declares no observability or opts out of nothing, so every shape
// recorded before the field encodes and hashes exactly as it did.
ObservabilityPolicy *ShapeObservabilityPolicy `json:"observability_policy,omitempty"`
// Applier is who applies this env when it DECLARES one: "forge" for a real
// environment forge applies on purpose (the platform's own env, which its
// own hub must not apply). Empty means undeclared, and omitted, so every
// shape recorded before the field encodes and hashes as it did.
Applier string `json:"applier,omitempty"`
}
Shape is the indexable projection of one env's render: what runs, where, which secrets it needs, which domains it binds, and one hash per object. It is computed in the SAME pass that writes the manifests, so the two cannot disagree, and it is small enough to store per bundle and read per env.
IT NEVER CARRIES A VALUE. Secrets are names and providers; objects are identities and hashes (a Secret's hash is taken after its values are redacted to their own hashes). Shape.Validate refuses anything that could hold a value, because a shape is stored forever and shown to every member.
func (Shape) Canonical ¶
Canonical returns the shape with every list sorted, so two projections of the same render encode to the same bytes regardless of render order.
func (Shape) Encode ¶
Encode is the canonical JSON of the shape, refused when it exceeds MaxShapeBytes.
func (Shape) SecretNamed ¶
func (s Shape) SecretNamed(name string) (ShapeSecret, bool)
SecretNamed returns the shape's secret of that name.
type ShapeAlert ¶
type ShapeAlert struct {
Alert string `json:"alert"`
Expr string `json:"expr"`
// For and Interval are Go duration strings ("5m", "1h30m").
For string `json:"for,omitempty"`
Interval string `json:"interval,omitempty"`
KeepFiringFor string `json:"keep_firing_for,omitempty"`
// Labels always carry `severity`: critical | warning | info.
Labels map[string]string `json:"labels,omitempty"`
Annotations map[string]string `json:"annotations,omitempty"`
// Source is AlertSourceUser or AlertSourceDefault.
Source string `json:"source"`
// DefaultID names the generated default this rule came from.
DefaultID string `json:"default_id,omitempty"`
}
ShapeAlert is one resolved alert rule: the Prometheus rule shape with the condition inside Expr, a single ClickHouse SELECT whose every row is a firing instance (non-`value` columns are labels, `value` the sample). The json tags are pkg/alerting's RuleSpec, so a consumer decodes this straight into it; KeepFiringFor and DefaultID are extra fields RuleSpec ignores.
type ShapeAlertRoute ¶
type ShapeAlertRoute struct {
Match map[string]string `json:"match,omitempty"`
Receiver string `json:"receiver"`
Continue bool `json:"continue,omitempty"`
}
ShapeAlertRoute routes alerts whose labels contain every Match pair (an empty Match matches all) to Receiver, a name the control plane resolves.
type ShapeControlPlaneSecret ¶
type ShapeControlPlaneSecret struct {
Name string `json:"name"`
Namespace string `json:"namespace"`
// Cluster is the kube context the Secret lands in, the same key the
// shape's workloads and objects use.
Cluster string `json:"cluster"`
// ConnectedCluster is the name `forge cluster connect` registered: the
// credential the control plane delivers with.
ConnectedCluster string `json:"connected_cluster"`
// Kind says what the control plane mints into it
// (ControlPlaneSecretTelemetryToken).
Kind string `json:"kind"`
Keys []string `json:"keys,omitempty"`
}
ShapeControlPlaneSecret is one Secret the control plane supplies: a DESTINATION and a KIND, never a value. Keys names what the control plane writes under the Secret, so a reader knows the contract without the kind's definition.
func (ShapeControlPlaneSecret) Key ¶
func (s ShapeControlPlaneSecret) Key() string
Key identifies the Secret within a shape: (cluster, namespace, name).
type ShapeDiff ¶
type ShapeDiff struct {
// LiveUnknown: there was no live shape to compare against (nothing
// recorded yet). Every candidate object then appears in Added, and a
// reader must say "no recorded config" rather than present the diff as
// a real comparison — empty and unknown must never render alike.
LiveUnknown bool `json:"live_unknown,omitempty"`
// KindChanged is set when the two shapes disagree about the env's kind.
// A kind is immutable, so this is always an error for a reader to
// surface, never a change that can be deployed.
KindChanged *KindChange `json:"kind_changed,omitempty"`
Added []ShapeObject `json:"added,omitempty"`
Removed []ShapeObject `json:"removed,omitempty"`
Changed []ObjectChange `json:"changed,omitempty"`
WorkloadsAdded []string `json:"workloads_added,omitempty"`
WorkloadsRemoved []string `json:"workloads_removed,omitempty"`
RuntimeChanges []RuntimeChange `json:"runtime_changes,omitempty"`
SecretsAdded []ShapeSecret `json:"secrets_added,omitempty"`
SecretsRemoved []ShapeSecret `json:"secrets_removed,omitempty"`
DomainsAdded []string `json:"domains_added,omitempty"`
DomainsRemoved []string `json:"domains_removed,omitempty"`
ClustersAdded []string `json:"clusters_added,omitempty"`
ClustersRemoved []string `json:"clusters_removed,omitempty"`
}
ShapeDiff is what changes between two shapes of one environment: the shape that is live (the applied bundle's, or the env's declared shape) and a candidate (a checkout's render, or the bundle about to be deployed).
It is a SET comparison over per-object hashes, which is why a shape carries one hash per object: a Live-vs-candidate diff never fetches or parses a manifest.
func DiffShapes ¶
DiffShapes compares a live shape (nil = nothing recorded) with a candidate. Pure, and shared by `forge env diff`, the deploy plan, and any backend that needs the same answer: one implementation, so a CLI and a server cannot disagree about what changed. Every list in the result is sorted.
type ShapeObject ¶
type ShapeObject struct {
Cluster string `json:"cluster"`
APIVersion string `json:"api_version,omitempty"`
Kind string `json:"kind"`
Namespace string `json:"namespace,omitempty"`
Name string `json:"name"`
// Workload is the forge workload the object belongs to
// (forge.dev/workload), for grouping.
Workload string `json:"workload,omitempty"`
// Hash is the digest of the pinned document after secret redaction:
// what drift detection compares the live object against.
Hash string `json:"hash"`
// ConfigHash is the digest of the same document with release-bound
// image references normalized to their artifact keys. Equal ConfigHash
// with a different Hash means only images changed.
ConfigHash string `json:"config_hash,omitempty"`
// Images maps the release artifact key → pinned digest for every
// release-bound image the object runs.
Images map[string]string `json:"images,omitempty"`
// Stateful marks an object that holds data beyond its own spec — a
// declared database's objects. The well-known stateful kinds
// ([statefulKinds]) are recognised without it.
Stateful bool `json:"stateful,omitempty"`
// Identity carries the attributes that give an object an EXTERNAL
// identity, which a deploy can silently replace: a Service's type and
// load balancer, an Ingress's static IP. Keys are closed
// ([IdentityKeys]) so the map can never become a place a value hides.
Identity map[string]string `json:"identity,omitempty"`
}
ShapeObject is one rendered Kubernetes object.
type ShapeObservabilityPolicy ¶
type ShapeObservabilityPolicy struct {
Enabled bool `json:"enabled"`
Disabled []string `json:"disabled,omitempty"`
// OptIn lists the categories the env turned ON that default off (only
// [ObsOptInReplay] today). Absent = none opted in.
OptIn []string `json:"opt_in,omitempty"`
}
ShapeObservabilityPolicy is the observability an env opted OUT of. Disabled lists categories, sorted; Enabled = false means everything is off (and Disabled then lists every category). Absent policy, or an empty Disabled with Enabled true, means everything is on.
func (*ShapeObservabilityPolicy) Off ¶
func (p *ShapeObservabilityPolicy) Off(category string) bool
Off reports whether category is opted out under this policy.
func (*ShapeObservabilityPolicy) OptedIn ¶
func (p *ShapeObservabilityPolicy) OptedIn(category string) bool
OptedIn reports whether the env turned an opt-in category on. Everything disabled is never opted in.
type ShapeSecret ¶
type ShapeSecret struct {
Name string `json:"name"`
// Provider is the env's secret provider for it: file | hosted |
// external | … (forge's provider discriminator).
Provider string `json:"provider"`
// DeclaredBy are the workloads that read it.
DeclaredBy []string `json:"declared_by,omitempty"`
}
ShapeSecret is one declared secret: a NAME, never a value.
type ShapeWorkload ¶
type ShapeWorkload struct {
Name string `json:"name"`
// Runtime is forge's runtime discriminator: host | compose | cluster |
// hosted (and the frontend runtimes for frontends).
Runtime string `json:"runtime"`
// Cluster is the kube context for a cluster workload.
Cluster string `json:"cluster,omitempty"`
// Artifact is the release artifact key its image comes from, if any.
Artifact string `json:"artifact,omitempty"`
Replicas *int `json:"replicas,omitempty"`
}
ShapeWorkload is one declared workload and where it runs.
type Source ¶
type Source struct {
// Repo is the repository the source is fetched from.
Repo string `json:"repo"`
// Ref is the tag/branch/sha as DECLARED (what a human edits).
Ref string `json:"ref"`
// Subdir is the path within the repo the component builds from.
Subdir string `json:"subdir,omitempty"`
// Commit is what Ref resolved to when the release was cut — the actual
// content address.
Commit string `json:"commit,omitempty"`
}
Source is the content-addressed identity of a source-built artifact.
type Stage ¶
type Stage struct {
Kind StageKind `json:"kind"`
// Name is which one: a check's name, an environment name, an artifact.
Name string `json:"name,omitempty"`
Status StageStatus `json:"status"`
// Env is the environment a promote or rollout stage concerns. Empty on
// a build, a cut, or a check that is not env-specific.
Env string `json:"env,omitempty"`
// PromotionID ties a promote or rollout stage to its ledger entry.
PromotionID string `json:"promotion_id,omitempty"`
StartedAt *time.Time `json:"started_at,omitempty"`
FinishedAt *time.Time `json:"finished_at,omitempty"`
URL string `json:"url,omitempty"`
Summary string `json:"summary,omitempty"`
}
Stage is one step of a run, as a reader sees it. It is a VIEW, assembled on read from the ledger — the release cut, the promotions, the gates carrying this run's id, and each promotion's derived rollout. Nothing constructs a Stage to store it.
type StageKind ¶
type StageKind string
StageKind says what part of a run a stage is. CLOSED.
The vocabulary covers the pipeline the primitives describe — build → cut → check → promote → rollout — where the narrative's "converge" and "verify" are not separate kinds:
- CONVERGE is the rollout: StageRollout is one promotion's progress toward the digests it pinned, derived on read from the deployment rows. Naming it separately would imply a second stored thing.
- VERIFY is a check: a post-deploy smoke or health probe is recorded as a Gate and appears as StageCheck, named "verify" or "smoke".
So every stage is either DERIVED from the ledger (cut, promote, rollout) or recorded as a gate (build, check). Nothing else is stored.
const ( // StageBuild is an artifact build. Recorded as a gate. StageBuild StageKind = "build" // StageCut is the release cut. Derived: the release row. StageCut StageKind = "cut" // StageCheck is any check — lint, test, smoke, wait, verify, a manual // sign-off. Recorded as a gate; Name carries which. StageCheck StageKind = "check" // StagePromote is one environment binding. Derived: the promotion row. StagePromote StageKind = "promote" // StageRollout is one promotion converging. Derived on read from the // deployment rows against that promotion's frozen pins. StageRollout StageKind = "rollout" )
func ParseStageKind ¶
ParseStageKind reads a stage kind and refuses anything outside the set.
func (*StageKind) UnmarshalJSON ¶
UnmarshalJSON decodes through ParseStageKind.
func (StageKind) Valid ¶
Valid reports whether k is one of StageKinds.
type StageStatus ¶
type StageStatus string
StageStatus is a stage's verdict. CLOSED. It is GateStatus plus StageRunning, because a stage can be observed WHILE it happens — a rollout in progress, a check still running — and a gate cannot: a gate is recorded once its check has finished.
const ( // StagePassed, StageFailed, StageSkipped and StageErrored mean exactly // what the matching [GateStatus] values mean. StagePassed StageStatus = StageStatus(GateStatusPassed) StageFailed StageStatus = StageStatus(GateStatusFailed) StageSkipped StageStatus = StageStatus(GateStatusSkipped) StageErrored StageStatus = StageStatus(GateStatusError) // StageRunning: the stage has started and has not reached a verdict. // A run with any running stage is not finished, which is why a "did // the whole run pass" check cannot treat this as either outcome. StageRunning StageStatus = "running" )
func ParseStageStatus ¶
func ParseStageStatus(s string) (StageStatus, error)
ParseStageStatus reads a stage status and refuses anything outside the set.
func StageStatusOfGate ¶
func StageStatusOfGate(g Gate) StageStatus
StageStatusOfGate is the one mapping from a recorded gate to its stage status. A gate has always finished, so it never maps to StageRunning, and a gate whose stored status was unrecognised maps to StageErrored — the same refusal to read an uninterpretable claim as a pass.
func (*StageStatus) UnmarshalJSON ¶
func (s *StageStatus) UnmarshalJSON(data []byte) error
UnmarshalJSON decodes through ParseStageStatus.
func (StageStatus) Valid ¶
func (s StageStatus) Valid() bool
Valid reports whether s is one of StageStatuses.
type Worktree ¶
type Worktree struct {
// Key is forge's devstack key: "" for the primary checkout, the
// sanitized linked-worktree name otherwise. It is the same key the
// checkout's dev-stack port block is held under.
Key string `json:"key"`
// Label is what a human calls the checkout: the branch, else the
// directory's base name. Never a path.
Label string `json:"label,omitempty"`
// Host names the machine: a daemon id, or a hash of the hostname.
// Never a raw hostname or path.
Host string `json:"host,omitempty"`
// Path is the checkout's absolute path, for DISPLAY on the machine that
// built it. It is stripped from anything sent to a hosted ledger
// ([Provenance.ForHosted]): a path names a user's home directory.
Path string `json:"path,omitempty"`
}
Worktree identifies one checkout. None of it is authority.