Lightning.WorkflowVersions (Lightning v2.15.16)
View SourceProvenance + comparison helpers for workflow heads.
- Persists append-only rows in
workflow_versionsand maintains a materializedworkflows.version_historyarray (12-char lowercase hex). record_version/3andrecord_versions/3are idempotent (ON CONFLICT DO NOTHING) and concurrency-safe (row lock, append without dupes).history_for/1andlatest_hash/1read the array first; when empty they fall back to the table with deterministic ordering by(inserted_at, id).classify/2andclassify_with_delta/2compare two histories (same/ahead/diverged).
Validation & invariants:
hashmust match^[a-f0-9]{12}$;sourcemust be"app"or"cli";(workflow_id, hash)is unique.
Designed for fast diffs and consistent “latest head” lookups.
Summary
Functions
Compares two histories and returns only the relation (no counts).
Compares two histories and returns the relation with a delta.
Ensures a workflow has at least one version recorded.
Generates a deterministic hash for a workflow based on its structure.
Returns the ordered history of heads for a workflow.
Returns the latest head for a workflow (or nil if none).
Records a single workflow head hash with provenance.
Types
@type hash() :: String.t()
Functions
Compares two histories and returns only the relation (no counts).
Wrapper around classify_with_delta/2.
Examples
iex> WorkflowVersions.classify(~w(a b), ~w(a b))
:same
iex> WorkflowVersions.classify(~w(a b), ~w(a b c))
{:ahead, :right}
iex> WorkflowVersions.classify(~w(a x), ~w(a y))
:diverged
@spec classify_with_delta([hash()], [hash()]) :: {:same, 0} | {:ahead, :left, non_neg_integer()} | {:ahead, :right, non_neg_integer()} | {:diverged, non_neg_integer()}
Compares two histories and returns the relation with a delta.
Possible results:
{:same, 0}— sequences are identical{:ahead, :right, n}—rightstrictly extendsleftbynitems{:ahead, :left, n}—leftstrictly extendsrightbynitems{:diverged, k}— sequences share a common prefix of lengthk, then diverge
Examples
iex> WorkflowVersions.classify_with_delta(~w(a b), ~w(a b c d))
{:ahead, :right, 2}
iex> WorkflowVersions.classify_with_delta(~w(a b x), ~w(a b y))
{:diverged, 2}
@spec ensure_version_recorded(Lightning.Workflows.Workflow.t()) :: {:ok, Lightning.Workflows.WorkflowVersion.t()} | {:error, term()}
Ensures a workflow has at least one version recorded.
If the workflow has no version history, this function will generate a hash from the current workflow state and record it.
Examples
iex> WorkflowVersions.ensure_version_recorded(workflow_without_versions)
{:ok, %WorkflowVersion{source: "app", hash: "abc123def456"}}
iex> WorkflowVersions.ensure_version_recorded(workflow_with_versions)
{:ok, %WorkflowVersion{source: "app", hash: "existing123"}}
@spec generate_hash(Lightning.Workflows.Workflow.t() | map()) :: binary()
Generates a deterministic hash for a workflow based on its structure.
Algorithm:
- Create a list
- Add the workflow name to the start of the list
- For each node (trigger, job and edge) in a consistent order
- Take only the relevant fields (e.g., name, body, adaptor)
- Sort by field name (for consistency)
- Add only the field VALUES to the list (keys are excluded)
- Numeric values (e.g., positions) are rounded up to integers
- Join the list into a string, no separator
- Hash the string with SHA 256
- Truncate the resulting string to 12 characters
Parameters
workflow— the workflow struct to hash
Returns
- A 12-character lowercase hex string
Examples
iex> WorkflowVersions.generate_hash(workflow)
"a1b2c3d4e5f6"
@spec history_for(Lightning.Workflows.Workflow.t()) :: [hash(), ...] | []
Returns the ordered history of heads for a workflow.
Queries workflow_versions ordered by inserted_at ASC, id ASC to provide
deterministic ordering for equal timestamps.
@spec latest_hash(Lightning.Workflows.Workflow.t()) :: hash() | nil
Returns the latest head for a workflow (or nil if none).
Queries workflow_versions with ORDER BY inserted_at DESC, id DESC LIMIT 1
for deterministic results.
@spec record_version(Lightning.Workflows.Workflow.t(), hash(), String.t()) :: {:ok, Lightning.Workflows.WorkflowVersion.t()} | {:error, term()}
Records a single workflow head hash with provenance.
This operation is idempotent and concurrency-safe:
- If the latest version has the same source and it's not the first version, it squashes (replaces) it
- If the hash+source already exists, it does nothing
- Otherwise, it inserts a new row
Parameters
workflow— the workflow owning the historyhash— 12-char lowercase hex (e.g.,"deadbeefcafe")source—"app"or"cli"(defaults to"app")
Examples
iex> WorkflowVersions.record_version(wf, "deadbeefcafe", "app")
{:ok, %WorkflowVersion{hash: "deadbeefcafe", source: "app"}}
iex> WorkflowVersions.record_version(wf, "NOT_HEX", "app")
{:error, :invalid_input}