Lightning.WorkflowVersions (Lightning v2.15.16)

View Source

Provenance + comparison helpers for workflow heads.

  • Persists append-only rows in workflow_versions and maintains a materialized workflows.version_history array (12-char lowercase hex).
  • record_version/3 and record_versions/3 are idempotent (ON CONFLICT DO NOTHING) and concurrency-safe (row lock, append without dupes).
  • history_for/1 and latest_hash/1 read the array first; when empty they fall back to the table with deterministic ordering by (inserted_at, id).
  • classify/2 and classify_with_delta/2 compare two histories (same/ahead/diverged).

Validation & invariants:

  • hash must match ^[a-f0-9]{12}$; source must 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

hash()

@type hash() :: String.t()

Functions

classify(left, right)

@spec classify([hash()], [hash()]) ::
  :same | {:ahead, :left} | {:ahead, :right} | :diverged

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

classify_with_delta(left, right)

@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}right strictly extends left by n items
  • {:ahead, :left, n}left strictly extends right by n items
  • {:diverged, k} — sequences share a common prefix of length k, 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}

ensure_version_recorded(workflow)

@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"}}

generate_hash(workflow)

@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"

history_for(workflow)

@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.

latest_hash(wf)

@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.

record_version(workflow, hash, source \\ "app")

@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 history
  • hash — 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}