8.5 KiB
Application Orchestration
Audience: developers and LLM coding agents changing internal/app.
Purpose
internal/app owns top-level application use cases: run, single-pipeline run, staged-source run, validate, inspect, manifest creation, reconcile-state planning/repair, prune planning/execution, and HTTP upload serving. It coordinates config loading, secret resolution, backend construction, source discovery, destination selection, publish planning/execution, state repair reporting, retention prune reporting, notification handoff, output projection, and upload coordination.
Inputs And Outputs
Inputs include app option structs, contexts, config paths, pipeline ids, local source roots, dry-run/force flags, output format, stdout writers, HTTP requests, and optional notifier implementations.
Outputs include RunReport, ReconcileStateReport, PrunePlanReport, PruneReport, validate/inspect/manifest results, CLI text/JSON projections, HTTP upload responses, upload status records, and errors. Destination-scoped failures can return a partial run report plus an aggregated error; fatal setup failures return before a complete report exists.
Boundaries
internal/app wires packages together but does not own manifest validation rules, destination state comparison, storage path rules, publish safety policy, transform rendering, config schema validation, or backend protocol behavior.
User-facing command parsing stays in internal/cli, including reconcile-state and prune flag validation and help text. User-facing config reference stays in docs/config.md. External contracts live under docs/integrations/.
Config Fields Used
The package consumes the loaded config.Config: server.http, secrets.directory, pipeline ids, source and destination backend fields, validation policy, publish policy, transform policy, path mapping, links, state policy, reconciliation policy, retention policy, and transfer policy.
Config fields are validated and defaulted by internal/config before app workflows use them.
Adapters Used
The app backend factory registers runtime storage adapters for local filesystem, SSH/SFTP, and S3-compatible storage. It resolves explicit credentials through the config-owned environment resolver before opening S3 backends.
The app layer registers default transforms, including Markdown-to-HTML, and supplies a transform resolver to publish planning. It uses notify.Noop when no notifier is supplied.
State And Manifest Behavior
Run workflows discover and validate source bundles through internal/bundle. Destination state actions are prepared and written through internal/publish and internal/state; the app layer records report projections of those actions and results.
Reconcile-state workflows load one configured pipeline/destination selector, open that destination root, parse the root .distributor.json, and report missing managed output records plus unmanaged storage entries. Managed output existence checks use storage Stat; unmanaged reporting uses bounded storage Walk and excludes .distributor.json plus all paths already recorded as managed. Apply mode removes missing managed output records from state and rewrites valid state only; dry-run reports the same repair without writing. Text output reports changed, would_change, or unchanged; JSON output uses the shared app envelope. It does not validate output digests, delete destination files, adopt unmanaged files, or rewrite invalid or mismatched state.
Prune planning consumes a parsed destination state document and a validated retention prune policy, then returns owner-scoped managed output records that would be pruned or preserved. Planning uses output updated_at timestamps, applies keep_latest before older_than when both are configured, and does not open storage, delete files, or rewrite state.
Prune execution loads one configured pipeline/destination selector, opens that destination root, parses the root .distributor.json, and builds a plan from the destination retention policy. Dry-run returns the same planned and preserved managed output records without deleting files or rewriting state. Apply mode deletes only planned managed output paths, never unmanaged files or .distributor.json, then removes confirmed deleted records from state and updates the state timestamp. If a delete fails after earlier deletes succeeded, it rewrites state only for the confirmed deletions and preserves records for the failed and unattempted outputs so a retry remains accurate. Text output reports changed, would_change, or unchanged; JSON output uses the shared app envelope.
HTTP uploads stage and validate archives before enqueueing a pipeline run with a local staged source root. Go producers can use the public pkg/upload package to create client-side gzip tar uploads for this server contract; internal/app remains the server-side orchestration boundary and does not import that producer package.
Upload idempotency is owned by the upload coordinator. Optional Idempotency-Key values are scoped to token id, pipeline id, and key. The coordinator reserves a key while staging is in progress, records the accepted run id with the validated source manifest identity after staging succeeds, returns the original accepted record for the same scoped key and same manifest, and rejects the same scoped key with a different manifest as a conflict.
Skip And Resume Behavior
Fan-out destinations are independent. A destination failure is recorded and does not prevent later destinations from being attempted. Run dry-run builds plans and reports without destination writes, destination state writes, notifier calls, or SSH known-host persistence. Reconcile-state dry-run reports missing managed records and unmanaged entries without rewriting state. Prune dry-run reports planned managed output deletes without deleting outputs or rewriting state.
HTTP upload status is in memory. Accepted jobs move through accepted, queued, running, succeeded, or failed states and expire after configured retention. Upload idempotency records are also memory-only, expire with the completed status record for their accepted run, and are cleared by process restart.
Failure Behavior
Runtime setup fails for config load, config validation, secret loading, or credential resolution errors. Source setup failures stop the affected run before destination planning. Destination open, planning, execution, and notification failures are recorded as destination failures where a partial result exists.
Reconcile-state setup fails unless the caller supplies a pipeline id and destination id that select one configured destination root. Single-owner state must match that pipeline/destination owner. Shared-root all-owner repair still uses the selected destination to identify the root, then applies repair across owners inside that root. Invalid, unreadable, or ambiguous state fails before any rewrite.
Prune setup fails unless the caller supplies a pipeline id and destination id that select one configured destination root. Single-owner state must match that pipeline/destination owner. Shared-root pruning is scoped to the selected owner and preserves unrelated owners. Invalid, unreadable, or ambiguous state fails before deletes or rewrites. Delete failures return a report with confirmed deletions and the failed output.
HTTP upload startup fails if upload tokens are missing, empty, or duplicated. Upload requests can fail during authentication, idempotency-key validation, content-type validation, idempotency conflict checks, queue admission, archive staging, source validation, or later publish execution.
Tests To Inspect
internal/app/*_test.gointernal/app/prune_test.gointernal/cli/reconcile_state_test.gointernal/cli/prune_test.gointernal/cli/root_test.gointernal/config/*_test.gointernal/ingest/*_test.gointernal/publish/*_test.go
Architectural Invariants
- App orchestration owns wiring, not low-level policy.
- Dry-run must not write outputs, destination state, notifier events, or SSH known-host entries.
- Fan-out destinations remain independent after a destination-scoped failure.
- Secret values are never printed; warnings may name variables only.
- Upload admission stages and validates a bundle before returning a run id.
- Idempotent upload retries compare normalized source manifest identity, not archive bytes.
- Runtime backend registration remains app-owned.
- Reconcile-state repairs state records only; it never deletes or adopts destination files.
- Prune execution deletes managed output paths only and preserves failed records for retry.