Documentation
¶
Overview ¶
Package manifest defines Shipwright's declarative workflow manifest schema (design.md D-H, the workflow-manifest spec): the typed Go structs a YAML workflow document decodes into, plus stages 1-3 of the fixed seven-stage validation pipeline — size-capped read + decode (stage 1), document identity (stage 2), and structure (stage 3).
Stages 4-7 (references, graph, provider resolution, value binding) belong to later packages (internal/workflow/interp, internal/workflow/graph, internal/workflow/providers) and are deliberately NOT implemented here — see Parse's doc comment.
Schema drift enforcement mirrors pkg/shipwright's guaranteed-surface golden (design.md D-E): testdata/schema.golden records the accepted field set, and any change to it forces a deliberate, reviewed `-update` diff and an explicit apiVersion decision in the same PR (design.md D-H).
Index ¶
- Constants
- func ValidateExecutable(m *Manifest) error
- func ValidateIdentity(m *Manifest) error
- func ValidateStructure(m *Manifest) error
- type ApprovalSpec
- type ArtifactsPolicy
- type ConcurrencySpec
- type DependenciesPolicy
- type Environment
- type ExecutionSpec
- type Manifest
- type Metadata
- type Policies
- type ProvidersPolicy
- type SecretSpec
- type SecretsPolicy
- type SourceSpec
- type Spec
- type Step
- type UnenforceableControlError
- type UsesSpec
Constants ¶
const MaxManifestBytes = 1 << 20
MaxManifestBytes is the hard cap on a manifest's raw byte size before decode (design.md D-H threat matrix item 1, tasks.md 4.4). A manifest read through Parse/ParseFile that exceeds this size is rejected before any YAML decoding happens.
Together with the typed schema (no field capable of holding arbitrary depth other than Step.With/Step.When, both bounded further by this raw byte cap), this is the deliberate defense against alias-amplification ("billion laughs") documents. yaml.v3's own internal alias-ratio protection is a backstop only — design.md is explicit that this package must not rely on it alone.
1 MiB is a test constant proposed in design.md, not a contract element (design.md Open Questions): tune it if a legitimate manifest ever needs more, or if the billion-laughs fixture in security_test.go demands a tighter bound.
Variables ¶
This section is empty.
Functions ¶
func ValidateExecutable ¶
ValidateExecutable is the execution-only fail-close gate (design.md D1). Deliberately NOT called by Parse/ParseFile, so a read-only command (e.g. --list-steps) still parses and displays these controls; the execution path must call this itself before any Dagger connection.
Fixed, first-offender order: approvals (sorted names) → policies.* → maxParallel > 1. No override.
func ValidateIdentity ¶
ValidateIdentity implements stage 2 of the fixed seven-stage validation pipeline (design.md D-H, workflow-manifest spec "Versioned Document Identity"): apiVersion and kind must be present and within their allowlists, and metadata.name must be present. Nothing about step structure is inspected here — that is ValidateStructure's job (stage 3).
func ValidateStructure ¶
ValidateStructure implements stage 3 of the fixed seven-stage validation pipeline (design.md D-H, workflow-manifest spec "capability Is The Contract, uses Is The Implementation"): every step id is non-empty and unique, capability is one of the five contract types, uses is present (a provider or a module), and uses.version is non-empty.
needs[]/when/with are parsed by the schema but their CONTENTS are not interpreted here — resolving needs against known step ids belongs to stage 4 (internal/workflow/interp) and stage 5 (internal/workflow/graph, cycle/reference validation), which this package deliberately does not implement.
Types ¶
type ApprovalSpec ¶
type ApprovalSpec struct {
Required []string `yaml:"required,omitempty"`
}
ApprovalSpec is declared, queryable metadata only — see Environment's doc comment and design.md D-M.
type ArtifactsPolicy ¶
type ArtifactsPolicy struct {
Immutable *bool `yaml:"immutable,omitempty"`
}
ArtifactsPolicy declares whether published artifacts must be immutable. No layer enforces it; see the Policies comment. Immutable is a pointer: declaring it `false` still asserts a guarantee this engine does not provide, and must fail closed too.
type ConcurrencySpec ¶
type ConcurrencySpec struct {
MaxParallel int `yaml:"maxParallel,omitempty"`
}
ConcurrencySpec bounds parallel execution within a wave.
type DependenciesPolicy ¶
type DependenciesPolicy struct {
ForbidCycles *bool `yaml:"forbidCycles,omitempty"`
}
DependenciesPolicy declares whether cycles are forbidden. Acyclicity is already enforced unconditionally at stage 5; see the Policies comment for why the flag is rejected even when declared `false`.
type Environment ¶
type Environment struct {
Approvals *ApprovalSpec `yaml:"approvals,omitempty"`
}
Environment names a deployment target and its declared approval metadata. The engine never blocks, queues, or waits on approvals (design.md D-M); any DECLARED approvals block — even an empty one — is rejected outright by ValidateExecutable (see the Policies comment). Approvals is a pointer so ValidateExecutable can distinguish "omitted" (nil) from "declared but empty" (non-nil, Required == nil): a manifest author writing `approvals: {}` still asserts a control this engine does not implement, and must fail closed the same as a populated block.
type ExecutionSpec ¶
type ExecutionSpec struct {
Concurrency ConcurrencySpec `yaml:"concurrency,omitempty"`
FailFast bool `yaml:"failFast,omitempty"`
Timeout string `yaml:"timeout,omitempty"`
}
ExecutionSpec declares scheduling intent. MaxParallel (via Concurrency) is validated and recorded as an upper bound (design.md D-K); the engine (Phase 8, out of scope here) is not required to use it to widen execution — sequential waves are a correct schedule for any maxParallel >= 1.
type Manifest ¶
type Manifest struct {
APIVersion string `yaml:"apiVersion"`
Kind string `yaml:"kind"`
Metadata Metadata `yaml:"metadata"`
Spec Spec `yaml:"spec"`
}
Manifest is the top-level document shape: APIVersion + Kind identify the schema version (stage 2, ValidateIdentity); Metadata names the workflow; Spec holds everything else.
func Parse ¶
Parse decodes and validates (stages 1-3 of the fixed seven-stage pipeline, design.md D-H) a manifest read from r. Stage 1 (size-capped read + decode) happens first and unconditionally: r's bytes are never handed to the YAML decoder directly, only a byte slice already proven to be within MaxManifestBytes (see readCapped).
func ParseFile ¶
ParseFile opens path, applies the MaxManifestBytes cap, decodes with KnownFields(true), and runs stages 2-3 validation (ValidateIdentity, ValidateStructure). It does not run stages 4-7 (references, graph, provider resolution, value binding) — those belong to internal/workflow/interp, internal/workflow/graph, and internal/workflow/providers.
type Metadata ¶
type Metadata struct {
Name string `yaml:"name"`
Description string `yaml:"description,omitempty"`
Labels map[string]string `yaml:"labels,omitempty"`
}
Metadata identifies the workflow document itself.
type Policies ¶
type Policies struct {
Secrets SecretsPolicy `yaml:"secrets,omitempty"`
Providers ProvidersPolicy `yaml:"providers,omitempty"`
Dependencies DependenciesPolicy `yaml:"dependencies,omitempty"`
Artifacts ArtifactsPolicy `yaml:"artifacts,omitempty"`
}
Policies declares intent this schema parses but does not itself enforce (workflow-manifest spec, "Policies Are Declared As Structured, Enforceable Schema Fields"). ANY declaration — even an explicit `false`, and even one whose guarantee already holds unconditionally, like Providers.RequireVersion or Dependencies.ForbidCycles — is rejected by ValidateExecutable, an execution-only stage NOT run by Parse/ParseFile. Each flag below is a pointer so ValidateExecutable can tell "omitted" (nil) apart from "declared" (non-nil, at any value) — see each field's comment.
type ProvidersPolicy ¶
type ProvidersPolicy struct {
RequireVersion *bool `yaml:"requireVersion,omitempty"`
}
ProvidersPolicy declares provider-version intent. An empty uses.version is already unconditionally rejected by ValidateStructure regardless of this flag; see the Policies doc comment for why the flag is rejected even when declared `false`.
type SecretSpec ¶
type SecretSpec struct {
FromEnv string `yaml:"fromEnv"`
}
SecretSpec is a NAME REFERENCE to a secret resolved from an external source (an environment variable today) — never an inline plaintext value (workflow-manifest spec, "Secrets Referenced By Name, Never Embedded As Plaintext"). This struct deliberately declares no field capable of holding a literal secret value: a manifest attempting `spec.secrets.<name>.value: "..."` fails at stage 1 decode (yaml.Decoder.KnownFields(true) rejects the unknown `value` field), not at a later validation stage — the schema shape itself is the enforcement mechanism, mirroring D-D's retyping of credentials to *dagger.Secret and the internal/pipelines/shared/docker.go client.SetSecret pattern this mechanism must reuse.
type SecretsPolicy ¶
type SecretsPolicy struct {
ForbidPlaintext *bool `yaml:"forbidPlaintext,omitempty"`
}
SecretsPolicy declares whether a secret reference is forbidden in a non-secret-typed field. No layer enforces it; see the Policies comment. ForbidPlaintext is a pointer: declaring it `false` still asserts a guarantee this engine does not provide, and must fail closed too.
type SourceSpec ¶
type SourceSpec struct {
Path string `yaml:"path,omitempty"`
Repo string `yaml:"repo,omitempty"`
Ref string `yaml:"ref,omitempty"`
AuthSecretRef string `yaml:"authSecretRef,omitempty"`
}
SourceSpec selects the workflow's input source: a local Path, or a Git Repo+Ref pinned pair with an optional AuthSecretRef. Which combination a given provider actually requires is a later-stage (references/graph) concern, not a structural one — this package parses the shape only.
type Spec ¶
type Spec struct {
Source SourceSpec `yaml:"source"`
Variables map[string]string `yaml:"variables,omitempty"`
Secrets map[string]SecretSpec `yaml:"secrets,omitempty"`
Steps []Step `yaml:"steps"`
Execution ExecutionSpec `yaml:"execution,omitempty"`
Environments map[string]Environment `yaml:"environments,omitempty"`
Policies Policies `yaml:"policies,omitempty"`
}
Spec is the workflow's declarative body.
type Step ¶
type Step struct {
ID string `yaml:"id"`
Capability string `yaml:"capability"`
Uses UsesSpec `yaml:"uses"`
Needs []string `yaml:"needs,omitempty"`
Input string `yaml:"input,omitempty"`
With map[string]any `yaml:"with,omitempty"`
When map[string][]string `yaml:"when,omitempty"`
Attempts *int `yaml:"attempts,omitempty"`
}
Step is one node in the workflow DAG. Capability is the contract (one of public-module-api's five capability interfaces, named build/test/ artifact/deploy/run); Uses is the implementation. Needs declares explicit DAG edges — a step's position in Spec.Steps never implies an edge (workflow-manifest spec, "Explicit DAG Edges Via needs[]"). Outputs is deliberately NOT a field here: every capability returns exactly one typed result, addressable as `${{ steps.<id>.output }}` (design.md D-H schema simplification — the proposal's field list was explicitly non-normative).
type UnenforceableControlError ¶
UnenforceableControlError reports a declared control that is parsed but not enforced at runtime (design.md D2).
func (*UnenforceableControlError) Error ¶
func (e *UnenforceableControlError) Error() string
type UsesSpec ¶
type UsesSpec struct {
Provider string `yaml:"provider,omitempty"`
Module string `yaml:"module,omitempty"`
Version string `yaml:"version"`
}
UsesSpec names the step's implementation: either an in-repo Provider or an external Module (never both meaningfully at once — which is used is a stage-6 provider-resolution concern), both pinned to a required Version. Version is design.md D-E's provider-version axis — independent of, and not covered by, ContractVersion's compatibility guarantee.