Files
distributor/docs/integrations/destination-state.md

8.3 KiB

Destination State Contract

Audience: operators, integrators, and maintainers who inspect or reason about destination .distributor.json files.

Each managed destination bundle path contains .distributor.json. This file is the destination sentinel and state record used for comparison, skip, replacement, and recovery decisions.

Single-Owner State Schema

Current state written by run uses schema version 2.

{
  "schema_version": 2,
  "distributor_version": "dev",
  "pipeline_id": "reports",
  "destination_id": "archive",
  "published_at": "2026-06-04T12:00:00Z",
  "created_at": "2026-06-04T12:00:00Z",
  "updated_at": "2026-06-04T12:00:00Z",
  "state": {
    "mode": "single_owner"
  },
  "reconciliation": {
    "mode": "replace"
  },
  "source": {
    "manifest": {
      "schema_version": 1,
      "id": "reports.example.2026-06-04",
      "digest": "sha256:...",
      "created": "2026-06-04T11:55:00Z",
      "files": [
        {"path": "report.md", "sha256": "sha256:...", "size": 1234}
      ]
    }
  },
  "links": {
    "primary_url": "https://reports.example.com/archive/report.html"
  },
  "outputs": [
    {
      "path": "report.html",
      "kind": "generated",
      "source_path": "report.md",
      "transform": "markdown_to_html",
      "url": "https://reports.example.com/archive/report.html",
      "sha256": "sha256:...",
      "size": 2345,
      "created_at": "2026-06-04T12:00:00Z",
      "updated_at": "2026-06-04T12:00:00Z"
    }
  ]
}

Required fields:

  • schema_version: must be 2 for newly written state.
  • pipeline_id: configured pipeline id that wrote the state.
  • destination_id: configured destination id that wrote the state.
  • published_at: RFC3339 publication timestamp.
  • created_at: RFC3339 timestamp for when this destination state record was first created.
  • updated_at: RFC3339 timestamp for the latest state update.
  • state.mode: must be single_owner.
  • reconciliation.mode: replace or merge.
  • source.manifest: embedded source bundle manifest.
  • outputs: output records array; empty is allowed, but the field is required.

Optional fields:

  • distributor_version: application version string when available.
  • links.primary_url: absolute HTTP or HTTPS URL selected by destination link policy.

Output Records

Each output record has:

  • path: destination-relative output path.
  • kind: source or generated.
  • source_path: source manifest path used for the output.
  • transform: required for generated outputs; omitted for copied source outputs.
  • url: optional absolute HTTP or HTTPS URL for the output.
  • sha256: lowercase sha256:<64 hex> digest of the output bytes.
  • size: output byte size, zero or greater.
  • created_at: RFC3339 timestamp for when this output path was first recorded as managed.
  • updated_at: RFC3339 timestamp for when this output path was last written or updated in state.

Output paths must be unique and use clean relative slash-separated path rules.

For replacement updates, newly planned outputs are written into state. For merge updates, retained output records preserve both timestamps, overwritten managed output records preserve created_at and receive a new updated_at, and new output records receive the current publication time for both fields.

Reconciliation Semantics

Destination reconciliation applies when destination state is older than the source and transfer policy permits replacement.

  • replace: delete managed output paths recorded in outputs plus .distributor.json, require the destination bundle path to be empty afterward, write the newly planned outputs, and write state whose outputs are exactly that new planned set.
  • merge: retain managed output paths omitted from the new plan, overwrite planned paths only when they are already recorded in existing state, fail when a newly planned path exists in storage but is not recorded as managed, and write state whose outputs are the cumulative managed set.

Top-level links.primary_url is selected from the newly planned outputs for the current publication. Retained outputs keep their existing per-output URL metadata.

If a merge publication fails after writing outputs, cleanup removes only newly created outputs from that failed attempt. Previously managed overwritten paths remain managed and are not removed by failed-attempt cleanup.

Comparison Semantics

distributor compares the current source manifest to destination state before writing:

  • No state and no content: publish new outputs.
  • No state and existing content: treat the destination as unmanaged.
  • Matching embedded source manifest: skip.
  • Same source id with older created: replace if policy allows.
  • Same source id with newer created: skip by default.
  • Same source id and same created with different digest: conflict.
  • Different source id, pipeline id, or destination id: conflict.
  • Invalid state JSON or invalid state fields: conflict.

Normal replacement deletes only managed output paths recorded in outputs plus .distributor.json. Merge publication retains omitted managed outputs. Forced replacement deletes the bounded destination bundle path.

Compatibility

distributor can read schema version 1 destination state for compatibility. When v1 state is read, it is treated as single-owner state with reconciliation.mode: replace. Missing top-level created_at and updated_at are inferred from published_at, and missing per-output timestamps are also inferred from published_at.

Newly written destination state from publish execution uses schema version 2.

Shared-Root State Schema

distributor can parse and validate shared-root destination state with schema version 3. Publish execution currently writes single-owner state.

{
  "schema_version": 3,
  "distributor_version": "dev",
  "created_at": "2026-06-04T12:00:00Z",
  "updated_at": "2026-06-04T12:10:00Z",
  "state": {
    "mode": "shared_root"
  },
  "owners": [
    {
      "pipeline_id": "reports",
      "destination_id": "archive",
      "reconciliation": {
        "mode": "merge"
      },
      "source": {
        "manifest": {
          "schema_version": 1,
          "id": "reports.example.2026-06-04",
          "digest": "sha256:...",
          "created": "2026-06-04T11:55:00Z",
          "files": [
            {"path": "report.md", "sha256": "sha256:...", "size": 1234}
          ]
        }
      },
      "links": {
        "primary_url": "https://reports.example.com/archive/report.html"
      }
    }
  ],
  "outputs": [
    {
      "path": "report.html",
      "kind": "generated",
      "source_path": "report.md",
      "transform": "markdown_to_html",
      "url": "https://reports.example.com/archive/report.html",
      "sha256": "sha256:...",
      "size": 2345,
      "pipeline_id": "reports",
      "destination_id": "archive",
      "source_id": "reports.example.2026-06-04",
      "source_digest": "sha256:...",
      "source_created": "2026-06-04T11:55:00Z",
      "created_at": "2026-06-04T12:00:00Z",
      "updated_at": "2026-06-04T12:10:00Z"
    }
  ]
}

Shared-root required fields:

  • schema_version: must be 3.
  • created_at: RFC3339 timestamp for when this shared-root state record was first created.
  • updated_at: RFC3339 timestamp for the latest shared-root state update.
  • state.mode: must be shared_root.
  • owners: owner records keyed by pipeline_id and destination_id; each owner records its latest source manifest and reconciliation mode.
  • outputs: output records for every managed path under the shared destination root.

Shared-root optional fields:

  • distributor_version: application version string when available.
  • owners[].links.primary_url: absolute HTTP or HTTPS URL selected by that owner's destination link policy.

Shared-root output records carry the same path, kind, source_path, transform, url, sha256, size, created_at, and updated_at fields as single-owner outputs. They also include the owner pipeline_id and destination_id, plus compact source identity fields source_id, source_digest, and source_created.

Boundaries

Destination state is internal managed state written by distributor. Operators may inspect it during recovery, but normal workflows should not edit it by hand. Source manifest.json is not copied as destination state.

Tests

Before changing this contract, inspect and run:

go test ./internal/state ./internal/publish