Architecture Boundaries¶
This document defines the package boundary rules for StageFreight. These are enforced by convention; violations should be caught in code review.
Package Roles¶
| Package | Role | May import |
|---|---|---|
src/artifact |
Published artifact identity and manifest (cross-cutting, no execution logic) | — (stdlib only) |
src/cli/cmd |
CLI adapters only — flag parsing, Cobra wiring, CI runner orchestration | everything |
src/build/docker |
Docker/buildx adapter: CLI execution, cosign signing, image inspection, buildx parsing, crucible engine, Dockerfile inventory; plus build orchestration (detect → plan → execute → publish) | build (parent), artifact, config, credentials, diag, output, pipeline, postbuild, registry |
src/build/pipeline |
Generic in-process phase runner and PipelineContext | artifact, build, config |
src/cistate |
Cross-job state ledger: per-subsystem fragments, merged into pipeline.json | ci |
src/postbuild |
Post-build integration glue (badges, retention, harbor, readme) | artifact, build, pipeline, registry |
src/registry |
Registry client + operational integration (verification, retention, referrers) | artifact, config, credentials, diag, retention |
src/build |
Shared build foundation: engine interfaces, plan/step/result, provenance model, OCI labeling, detection, tags, version | config, gitver; credentials/diag in execution paths |
Scratch Rules¶
Scratchis for intra-package ephemeral state only — keys written and read within the same package.- Cross-package pipeline state uses typed fields on
PipelineContext(Manifest,BuildPlan). - Keys are namespaced by package prefix:
docker.*,binary.*. - Never add a Scratch key readable from a different package. Promote it to a typed field instead.
Service Function Rule¶
Commands invocable from CI runners must expose an explicit request-based function.
Ctx lives inside the request — matches the docker.Request pattern already established:
type XxxRequest struct {
Ctx context.Context
// ... all other inputs explicit, no package vars
}
func RunXxx(req XxxRequest) error { ... }
The Cobra RunE wrapper builds the request from flag vars and calls RunXxx.
CI runners call RunXxx directly — no flag var mutation.
Any exported or testable non-Cobra function in src/cli/cmd must not read cfg.*.
Config must arrive by parameter. The Cobra wrapper is the only global-aware edge.
Import Audit Notes¶
src/config: pure data model, zero internal imports — safe to import anywhere.src/props: no internal imports — stable leaf node.narrator→props: one-way, intentional — healthy.postbuild: high fan-in by design — acceptable as long as it stays post-build-only.src/output→lint: only upward coupling in output; acceptable.
Rot Risk Watchlist¶
These are the areas most likely to accumulate coupling if not actively maintained.
| Area | Risk | Signal to watch for |
|---|---|---|
src/artifact |
Foundation-package drift | Any internal imports or non-manifest execution/provider logic appearing |
src/postbuild |
Temporal junk drawer | New files that aren't badge/readme/retention/harbor |
src/build |
Domain sprawl | Files importing forge or release (strong smell) |
src/cli/cmd |
cfg-as-global creep | Any non-Cobra function reading cfg.* without a param |
src/registry |
Dependency regression | Any import of src/build (hard boundary after artifact extraction) |
narrator_run.go helpers |
cfg swap recurrence | Any save/restore pattern on a package global |
output → lint |
Upward coupling | output importing any domain package other than lint |
Import Contracts¶
Tight boundary rules for the hottest packages. Violations should be caught in code review. These rules are intended for future automated checking.
src/artifact¶
- Identity: cross-cutting published artifact identity and manifest — not execution logic
- Permitted: stdlib only (zero internal imports)
- Forbidden: everything internal — this is a foundational leaf package
- Rules:
- Owns published image/binary/archive metadata; build and registry both import it
- Contains only data structures plus serialization/persistence directly related to artifact identity and manifest handling. No execution logic, orchestration, network I/O, or provider integration.
src/cli/cmd¶
- Permitted: any service/domain package (this is the dependency graph leaf)
- Forbidden: must not be imported by any package
- Rules:
- Non-Cobra helpers must not read package globals (cfg.*, flag vars, mutable package state)
- Command wrappers may translate flags/config into request structs, but business logic should not accumulate here
- Command wrappers must not pass partially-initialized config or mutate shared state before calling service functions. All inputs must be explicit.
- Non-wrapper helpers should prefer narrow params or explicit request structs over wrapper-owned state fanout
src/build (root package files only)¶
- Identity: shared build foundation — NOT a Docker implementation home
- Permitted:
config,gitver,artifact - Permitted in execution/proving paths only:
credentials,diag - Forbidden:
forge,release,cli,postbuild - Forbidden in root package:
registry - Rules:
- Root must NOT contain CLI adapter code (docker/buildx/cosign/podman command invocation)
- Shared build semantics (provenance model, OCI labels, plan fingerprinting) belong here
- Docker adapter code belongs in
build/docker/ - Binary-specific code (gobuild.go, archive.go) stays until volume justifies
build/binary/ - Subpackage note:
build/dockerandbuild/pipelineare higher-level coordinators and may have wider imports consistent with their orchestration role
src/registry¶
- Identity: operational registry integration layer, not a pure registry client
- Permitted:
artifact,config,credentials,diag,retention - Forbidden:
build(hard boundary — do not reintroduce),forge,release,cli,postbuild,narrator - Rules:
- Provider logic and registry-domain operations belong here
- Registry operates on artifact types but does not define or mutate artifact identity semantics
- Retention application is allowed here only as long as registry remains the execution home for registry retention operations
src/config¶
- Zero internal imports. Must stay a pure data model.
src/props¶
- Zero internal imports. Stable leaf node.
src/postbuild¶
- Identity: post-build integration layer for completed artifacts only
- Permitted:
artifact,build,registry,config,output - Forbidden:
forge,release,cli,narrator - Rule: only work that is strictly downstream of completed build results belongs here
- Smells (should trigger review pushback): release authoring, pipeline triggering, configuration mutation, provenance generation, signing, notification fanout
Layer Rigor¶
Review rules depend on which layer code belongs to. Identify the layer first.
| Layer | Example | Rules |
|---|---|---|
| Identity | artifact.ArtifactID |
Typed; constructed only by NewArtifactID; propagated unchanged; never recomputed from fields |
| Truth model | OutputsManifest, ResultsManifest, *ExecutionView |
Schema-validated; deterministic; joined only on ArtifactID |
| Configuration | RegistryConfig.Provider, RegistryConfig.Credentials |
Authoritative for routing, auth, and policy |
| Presentation | providerFromHost, release-notes labels, display strings |
Heuristics allowed; misclassification affects labels only |
A presentation helper is correct when a misclassification renders as a bad label and never
changes behavior. Verify that its output does not flow into routing, auth, or policy, and
record that boundary in a comment; do not prune working heuristics (e.g. substring matches
for self-hosted harbor.* / gitea.* hosts) to make a display function "safer".
Presentation must never fail a completed operation — URL and label builders degrade, never
panic.
Git Transport¶
Commit, tag, and sync networking goes through go-git (gitstate.Transport), in-process;
StageFreight never runs a git binary, and the no-git-exec lint holds that (tests alone
may, with git provided by the substrate). Local remotes use go-git's in-process server.
Each remote's credential is resolved on its first network use, host-bound:
- SSH —
SSH_PRIVATE_KEY, else the SSH agent, else~/.ssh/config'sIdentityFilefor the host, else~/.ssh/id_{ed25519,ecdsa,rsa}.~/.ssh/configalso suppliesUser,HostnameandPort;ProxyJumpis not supported. - HTTPS — only the credential of the configured forge that owns the host; any other host is reached anonymously. Git credential helpers are not consulted.
The commit replay engine (src/commit/gogit_replay.go) rebuilds trees in-process and is
guarded by a path-set integrity check (staged paths must equal the source commit's change
paths; mismatch hard-resets).
On-Disk Layout¶
src/paths (stdlib-only leaf) is the single source of truth for where StageFreight
writes. Every path belongs to one bucket:
| Bucket | Location | Accessor | Lifecycle |
|---|---|---|---|
| Durable | .stagefreight/ allowlisted entries (scribe/, preset-cache/, inventory/, toolchains.lock) |
paths.Durable |
Committed and reviewed |
| Per-run I/O | .stagefreight/ everything else (manifests, content store, reports, security, deps, dist/, pipeline.json) |
paths.Ephemeral |
Gitignored; forwarded between CI jobs; regenerated each run |
| Scratch | .stagefreight/.tmp/ |
paths.Scratch |
Gitignored; safe to delete |
| Cache | out of tree (paths.ResolveCacheRoot) |
paths.Cache |
Cross-run reuse; never a correctness dependency |
| State | /var/lib/stagefreight/ |
paths.State |
Host-scoped (signing/KMS) |
- Per-run I/O is in-tree only because forge artifact/report paths must resolve inside the
project directory. It stays flat at the top of
.stagefreight/for discoverability. .stagefreight/.gitignoreis a self-contained allowlist:/*ignores everything, then!lines carve out the durable set. A new output is ignored by default; a new durable entry must be added to the allowlist and the classifier together (a completeness test ties them). The commit backend plants it viaworkspace.EnsureGitignorebefore staging, so every managed repo gets it without editing its root.gitignore.- Cache root priority:
--cache→SF_CACHE_ROOT→/stagefreightif writable (the container bind-mount convention) →${XDG_CACHE_HOME:-~/.cache}/stagefreight(native) → none (each consumer falls back to its own ephemeral location). The Actions render persists/stagefreight/cachewithactions/cache;SF_CACHE_ROOTis the seam for pointing the cache at a forge-cacheable in-workspace path. .stagefreight/toolchains(src/toolchain/cache.go) is the last-resort toolchain install location when no cache root resolves. It is cache (ignored, wiped by checkout), not durable state.
Enforcement¶
Today these boundaries are enforced by convention and code review, not a mechanical
gate — the rules above are the review checklist. A future static check could fail CI on the
clear-cut violations (cfg. outside the Cobra wrappers in src/cli/cmd, src/registry
importing src/build, src/artifact importing an internal package), but that check isn't
wired yet — treat this page as the authority in the meantime.