Module: Browserctl::Workflow::PromotionLedger

Defined in:
lib/browserctl/workflow/promotion_ledger.rb

Overview

Append-only JSONL ledger of ‘workflow run –check` outcomes per workflow. Used as the gate for `workflow promote`: only workflows with a sufficient streak of clean runs are eligible for promotion to `~/.browserctl/workflows/`.

Record schema (one JSONL line):

{ "ts": "2026-05-10T12:00:00Z", "workflow": "name", "verdict": "clean" }

Constant Summary collapse

LEDGER_BASENAME =
"check_ledger.jsonl"
DEFAULT_THRESHOLD =
3
VALID_VERDICTS =
%i[clean drift fail].freeze

Class Method Summary collapse

Class Method Details

.clean_streak(workflow:, path: ledger_path) ⇒ Integer

Count the trailing streak of :clean verdicts for a workflow. A non-clean verdict resets the streak. Drift and fail both break it — the gate is intentionally strict; users can override with –force.

Returns:

  • (Integer)


48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/browserctl/workflow/promotion_ledger.rb', line 48

def clean_streak(workflow:, path: ledger_path)
  return 0 unless File.exist?(path)

  streak = 0
  File.foreach(path) do |line|
    entry = parse(line) or next
    next unless entry["workflow"] == workflow.to_s

    if entry["verdict"] == "clean"
      streak += 1
    else
      streak = 0
    end
  end
  streak
end

.ledger_pathObject



23
24
25
# File 'lib/browserctl/workflow/promotion_ledger.rb', line 23

def ledger_path
  File.join(Browserctl::BROWSERCTL_DIR, LEDGER_BASENAME)
end

.parse(line) ⇒ Object



65
66
67
68
69
# File 'lib/browserctl/workflow/promotion_ledger.rb', line 65

def parse(line)
  JSON.parse(line)
rescue JSON::ParserError
  nil
end

.record(workflow:, verdict:, path: ledger_path, at: Time.now.utc) ⇒ Object

Append a verdict for a workflow run.

Parameters:

  • workflow (String)
  • verdict (Symbol)

    :clean, :drift, or :fail

  • path (String) (defaults to: ledger_path)

    override (testing)



31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/browserctl/workflow/promotion_ledger.rb', line 31

def record(workflow:, verdict:, path: ledger_path, at: Time.now.utc)
  return unless VALID_VERDICTS.include?(verdict)

  FileUtils.mkdir_p(File.dirname(path))
  File.open(path, "a") do |f|
    f.puts JSON.generate(
      ts: at.iso8601,
      workflow: workflow.to_s,
      verdict: verdict.to_s
    )
  end
end