jazari 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +38 -0
- data/LICENSE +9 -0
- data/README.md +255 -0
- data/app/models/jazari/anchor.rb +16 -0
- data/app/models/jazari/application_record.rb +10 -0
- data/app/models/jazari/recipe_record.rb +29 -0
- data/app/models/jazari/run.rb +28 -0
- data/app/models/jazari/runbook.rb +16 -0
- data/lib/generators/jazari/install/install_generator.rb +36 -0
- data/lib/generators/jazari/install/templates/create_jazari_tables.rb +81 -0
- data/lib/jazari/anchors.rb +49 -0
- data/lib/jazari/checklist.rb +62 -0
- data/lib/jazari/errors.rb +17 -0
- data/lib/jazari/mcp/handler.rb +130 -0
- data/lib/jazari/operations.rb +264 -0
- data/lib/jazari/railtie.rb +24 -0
- data/lib/jazari/recipe.rb +44 -0
- data/lib/jazari/recipe_registry.rb +37 -0
- data/lib/jazari/resolved_runbook.rb +24 -0
- data/lib/jazari/runs.rb +179 -0
- data/lib/jazari/targets.rb +15 -0
- data/lib/jazari/version.rb +5 -0
- data/lib/jazari.rb +143 -0
- metadata +97 -0
data/lib/jazari/runs.rb
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module Jazari
|
|
7
|
+
# The Run layer.
|
|
8
|
+
#
|
|
9
|
+
# Idempotency is a property of the RECIPE, not a global rule: verifying a
|
|
10
|
+
# backup should happen once a day; triaging an incident may happen five times.
|
|
11
|
+
# Under `once_per_calendar_day` a partial unique index over
|
|
12
|
+
# (recipe_id, COALESCE(subject_type,''), COALESCE(subject_id,0), started_on)
|
|
13
|
+
# enforces it — the COALESCE matters because a bare NULL subject (a queue run)
|
|
14
|
+
# would otherwise be unconstrained, since NULL != NULL in a unique index.
|
|
15
|
+
module Runs
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
# Insert FIRST, then rescue the unique violation and select the winner.
|
|
19
|
+
# A find-then-insert races under concurrent writers — the same defect class
|
|
20
|
+
# the revision guard exists to prevent.
|
|
21
|
+
def open(target:, actor_ref:, now: Time.now.utc)
|
|
22
|
+
recipe = RecipeRegistry.fetch(target.recipe_id)
|
|
23
|
+
subject = subject_for(target)
|
|
24
|
+
started_at = now.utc
|
|
25
|
+
|
|
26
|
+
attributes = {
|
|
27
|
+
recipe_id: recipe.id,
|
|
28
|
+
source_digest: recipe.digest,
|
|
29
|
+
subject_type: subject&.class&.name,
|
|
30
|
+
subject_id: subject&.id,
|
|
31
|
+
actor_ref: actor_ref.to_s,
|
|
32
|
+
started_at: started_at,
|
|
33
|
+
# The UTC calendar day. Never server-local: one nightly ritual must not
|
|
34
|
+
# land on two different days depending on which region's box called it.
|
|
35
|
+
started_on: started_at.to_date,
|
|
36
|
+
idempotency_policy: recipe.run_policy,
|
|
37
|
+
# The run is bound to the canon it opened against. Without this, an
|
|
38
|
+
# operator editing a recipe mid-run makes the in-flight run unable to
|
|
39
|
+
# tick its own steps, and able to tick steps that did not exist when it
|
|
40
|
+
# started. source_digest alone is provenance, not protection.
|
|
41
|
+
checklist_snapshot: recipe.checklist.map { |i| i.transform_keys(&:to_s) },
|
|
42
|
+
ticks: [], evidence: []
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
attempts = 0
|
|
46
|
+
begin
|
|
47
|
+
attempts += 1
|
|
48
|
+
run = Run.create!(attributes)
|
|
49
|
+
{ run: run, created: true, idempotent_reuse: false }
|
|
50
|
+
rescue ActiveRecord::RecordNotUnique => error
|
|
51
|
+
# Only a once-per-day recipe can collide on the idempotency index. Any
|
|
52
|
+
# other unique violation belongs to a constraint we do not own — a host
|
|
53
|
+
# index, say — and must never be reinterpreted as reuse.
|
|
54
|
+
raise unless recipe.once_per_calendar_day?
|
|
55
|
+
|
|
56
|
+
existing = find_days_run(attributes)
|
|
57
|
+
# The winner can be deleted between our failed INSERT and this SELECT.
|
|
58
|
+
# Retry once: on the second pass the row is gone and the INSERT wins.
|
|
59
|
+
retry if existing.nil? && attempts < 2
|
|
60
|
+
|
|
61
|
+
# No row on the idempotency key means this violation came from some
|
|
62
|
+
# OTHER constraint. Surfacing our own error here would hide the host's
|
|
63
|
+
# real one, so re-raise theirs untouched.
|
|
64
|
+
raise error if existing.nil?
|
|
65
|
+
|
|
66
|
+
{ run: existing, created: false, idempotent_reuse: true }
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def tick(run:, expected_revision:, item_id:, done:, actor_ref:, note: nil, now: Time.now.utc)
|
|
71
|
+
mutate(run, expected_revision) do |record|
|
|
72
|
+
raise RunClosed, "run #{record.id} is already closed" if record.closed?
|
|
73
|
+
|
|
74
|
+
snapshot = stored(record.checklist_snapshot)
|
|
75
|
+
if snapshot.empty?
|
|
76
|
+
# Every run written by this gem carries its opening checklist. An
|
|
77
|
+
# empty one means the row predates the snapshot column, so any
|
|
78
|
+
# migration that adds it must backfill rather than default to [].
|
|
79
|
+
raise InvalidRunbook, "run #{record.id} has no checklist snapshot; backfill required"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
known = snapshot.map { |item| item["id"] }
|
|
83
|
+
raise ItemNotFound, "unknown checklist item #{item_id}" unless known.include?(item_id.to_s)
|
|
84
|
+
|
|
85
|
+
ticks = stored(record.ticks).reject { |t| t["id"] == item_id.to_s }
|
|
86
|
+
ticks << { "id" => item_id.to_s, "done" => done == true, "at" => now.utc.iso8601,
|
|
87
|
+
"actor_ref" => actor_ref.to_s, "note" => note&.to_s }
|
|
88
|
+
record.ticks = ticks
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def attach_evidence(run:, expected_revision:, item_id:, kind:, value:, now: Time.now.utc)
|
|
93
|
+
raise InvalidRunbook, "unknown evidence kind #{kind.inspect}" unless EVIDENCE_KINDS.include?(kind.to_s)
|
|
94
|
+
|
|
95
|
+
mutate(run, expected_revision) do |record|
|
|
96
|
+
raise RunClosed, "run #{record.id} is already closed" if record.closed?
|
|
97
|
+
|
|
98
|
+
record.evidence = stored(record.evidence) + [
|
|
99
|
+
{ "item_id" => item_id&.to_s, "kind" => kind.to_s,
|
|
100
|
+
"value" => value.to_s[0, MAX_EVIDENCE], "at" => now.utc.iso8601 }
|
|
101
|
+
]
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def close(run:, expected_revision:, outcome:, now: Time.now.utc)
|
|
106
|
+
mutate(run, expected_revision) do |record|
|
|
107
|
+
raise RunClosed, "run #{record.id} is already closed" if record.closed?
|
|
108
|
+
raise InvalidRunbook, "unknown outcome #{outcome.inspect}" unless Run::OUTCOMES.include?(outcome.to_s)
|
|
109
|
+
|
|
110
|
+
record.outcome = outcome.to_s
|
|
111
|
+
record.finished_at = now.utc
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def last(target:)
|
|
116
|
+
recipe_id = target.recipe_id.to_s
|
|
117
|
+
subject = subject_for(target, strict: false)
|
|
118
|
+
# An anchor target whose anchor does not exist yet cannot have runs. It
|
|
119
|
+
# must NOT fall through to the nil-subject branch, which would return
|
|
120
|
+
# queue runs belonging to a different target entirely.
|
|
121
|
+
return nil if target.is_a?(AnchorTarget) && subject.nil?
|
|
122
|
+
|
|
123
|
+
scope = Run.where(recipe_id: recipe_id)
|
|
124
|
+
scope = if subject
|
|
125
|
+
scope.where(subject_type: subject.class.name, subject_id: subject.id)
|
|
126
|
+
else
|
|
127
|
+
scope.where(subject_type: nil, subject_id: nil)
|
|
128
|
+
end
|
|
129
|
+
scope.order(started_at: :desc).first
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# -- internals ---------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
EVIDENCE_KINDS = %w[output url sha count note].freeze
|
|
135
|
+
MAX_EVIDENCE = 2_000
|
|
136
|
+
|
|
137
|
+
def mutate(run, expected_revision)
|
|
138
|
+
record = run.is_a?(Run) ? run : Run.find_by(id: run)
|
|
139
|
+
raise TargetNotFound, "unknown run" unless record
|
|
140
|
+
|
|
141
|
+
record.with_lock do
|
|
142
|
+
unless record.lock_version.to_s == expected_revision.to_s
|
|
143
|
+
raise RevisionConflict, "expected revision #{expected_revision}, actual #{record.lock_version}"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
yield record
|
|
147
|
+
record.save!
|
|
148
|
+
end
|
|
149
|
+
record
|
|
150
|
+
end
|
|
151
|
+
private_class_method :mutate
|
|
152
|
+
|
|
153
|
+
def find_days_run(attributes)
|
|
154
|
+
Run.where(
|
|
155
|
+
recipe_id: attributes[:recipe_id],
|
|
156
|
+
subject_type: attributes[:subject_type],
|
|
157
|
+
subject_id: attributes[:subject_id],
|
|
158
|
+
started_on: attributes[:started_on],
|
|
159
|
+
idempotency_policy: attributes[:idempotency_policy]
|
|
160
|
+
).order(started_at: :desc).first
|
|
161
|
+
end
|
|
162
|
+
private_class_method :find_days_run
|
|
163
|
+
|
|
164
|
+
# strict: true — a write path; an unresolvable anchor fails closed.
|
|
165
|
+
# strict: false — a read path; a not-yet-created anchor is simply absent.
|
|
166
|
+
def subject_for(target, strict: true)
|
|
167
|
+
case target
|
|
168
|
+
when RecordTarget then target.runbookable
|
|
169
|
+
when AnchorTarget then Anchors.resolve(target, strict: strict)
|
|
170
|
+
when QueueTarget then nil
|
|
171
|
+
else raise TargetNotFound, "unsupported target"
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
private_class_method :subject_for
|
|
175
|
+
|
|
176
|
+
def stored(value) = Array(value).map { |h| h.to_h.transform_keys(&:to_s) }
|
|
177
|
+
private_class_method :stored
|
|
178
|
+
end
|
|
179
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jazari
|
|
4
|
+
# Hosts authorize the actor FIRST, then construct exactly one of these. The
|
|
5
|
+
# domain never accepts a raw id, an arbitrary record, or an actor.
|
|
6
|
+
RecordTarget = Data.define(:runbookable, :public_reference, :recipe_id)
|
|
7
|
+
|
|
8
|
+
# Any subject that is not an ActiveRecord row: a JSON-tree node, a file path,
|
|
9
|
+
# a DNS zone, a document. `scope` is host-registered.
|
|
10
|
+
AnchorTarget = Data.define(:scope_type, :scope_id, :key, :public_reference, :recipe_id)
|
|
11
|
+
|
|
12
|
+
# A stable name for a ritual that outlives any record. Read-only by design:
|
|
13
|
+
# a ritual has exactly one editable home, and that is its recipe.
|
|
14
|
+
QueueTarget = Data.define(:queue, :public_reference, :recipe_id)
|
|
15
|
+
end
|
data/lib/jazari.rb
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "jazari/version"
|
|
4
|
+
require "jazari/errors"
|
|
5
|
+
require "jazari/checklist"
|
|
6
|
+
require "jazari/recipe"
|
|
7
|
+
require "jazari/targets"
|
|
8
|
+
require "jazari/anchors"
|
|
9
|
+
require "jazari/resolved_runbook"
|
|
10
|
+
require "jazari/recipe_registry"
|
|
11
|
+
require "jazari/runs"
|
|
12
|
+
require "jazari/operations"
|
|
13
|
+
require "jazari/mcp/handler"
|
|
14
|
+
require "jazari/railtie" if defined?(::Rails::Railtie)
|
|
15
|
+
|
|
16
|
+
# Addressable operating procedures.
|
|
17
|
+
#
|
|
18
|
+
# RECIPE the canon - how this ritual is done. Data, not code.
|
|
19
|
+
# RUNBOOK one subject's override of it.
|
|
20
|
+
# QUEUE a stable name for a ritual that outlives any record.
|
|
21
|
+
# RUN one execution - who, when, which ticks, what evidence.
|
|
22
|
+
#
|
|
23
|
+
# This module is the only public mutation interface. It never accepts a raw id,
|
|
24
|
+
# an arbitrary record, or an actor: hosts authorize first, then pass exactly one
|
|
25
|
+
# immutable target value.
|
|
26
|
+
module Jazari
|
|
27
|
+
class << self
|
|
28
|
+
attr_accessor :configuration
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
Configuration = Struct.new(:actor_ref, :on_subject_destroyed, :anchor_scopes,
|
|
32
|
+
:table_prefix, :table_names) do
|
|
33
|
+
def initialize(*)
|
|
34
|
+
super
|
|
35
|
+
self.anchor_scopes ||= {}
|
|
36
|
+
self.table_prefix ||= "jazari_"
|
|
37
|
+
# Per-table overrides. A prefix alone is not enough for a host adopting
|
|
38
|
+
# tables it already has: existing names rarely follow one scheme, and
|
|
39
|
+
# renaming live tables in the same deploy as a cut-over is exactly what
|
|
40
|
+
# the adoption plan forbids. Any key omitted falls back to the prefix.
|
|
41
|
+
self.table_names ||= {}
|
|
42
|
+
self.actor_ref ||= ->(actor) { "actor:#{actor.object_id}" }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Fail at boot, not at first call.
|
|
46
|
+
def validate!
|
|
47
|
+
anchor_scopes.each_key do |scope|
|
|
48
|
+
raise ArgumentError, "anchor scope #{scope.inspect} must be a String" unless scope.is_a?(String)
|
|
49
|
+
end
|
|
50
|
+
true
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
TABLES = { recipes: "recipes", runbooks: "runbooks", anchors: "anchors", runs: "runs" }.freeze
|
|
55
|
+
|
|
56
|
+
def self.configure
|
|
57
|
+
self.configuration ||= Configuration.new
|
|
58
|
+
yield configuration if block_given?
|
|
59
|
+
configuration.validate!
|
|
60
|
+
apply_table_names!
|
|
61
|
+
configuration
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.table_name_for(key)
|
|
65
|
+
TABLES.fetch(key) # raise early on an unknown key
|
|
66
|
+
config.table_names[key]&.to_s || "#{config.table_prefix}#{TABLES.fetch(key)}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# A host may adopt these tables under existing names rather than renaming
|
|
70
|
+
# live tables in the same deploy as the cut-over.
|
|
71
|
+
#
|
|
72
|
+
# CONSTRAINT: ActiveRecord table names are process-global class state, so the
|
|
73
|
+
# prefix is a BOOT-TIME setting for the whole process. It is not per-request,
|
|
74
|
+
# per-thread, or per-tenant, and two hosts in one process cannot hold
|
|
75
|
+
# different prefixes. Call `configure` once, at boot, after the models are
|
|
76
|
+
# loaded; `models_loaded?` reports whether the binding actually took effect
|
|
77
|
+
# so a host can assert it instead of silently running on default names.
|
|
78
|
+
def self.models_loaded? = const_defined?(:RecipeRecord)
|
|
79
|
+
|
|
80
|
+
def self.apply_table_names!
|
|
81
|
+
return false unless models_loaded?
|
|
82
|
+
|
|
83
|
+
{ RecipeRecord: :recipes, Runbook: :runbooks, Anchor: :anchors, Run: :runs }.each do |klass, key|
|
|
84
|
+
const_get(klass).table_name = table_name_for(key)
|
|
85
|
+
end
|
|
86
|
+
true
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def self.config = configuration || configure
|
|
90
|
+
|
|
91
|
+
# The documented public interface (spec 02 section 3). `Runs` is the
|
|
92
|
+
# implementation; these are the names hosts and the MCP handler call.
|
|
93
|
+
class << self
|
|
94
|
+
def open_run(target:, actor_ref:, now: Time.now.utc)
|
|
95
|
+
Runs.open(target: target, actor_ref: actor_ref, now: now)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def tick(run:, expected_revision:, item_id:, done:, actor_ref:, note: nil)
|
|
99
|
+
Runs.tick(run: run, expected_revision: expected_revision, item_id: item_id,
|
|
100
|
+
done: done, actor_ref: actor_ref, note: note)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def attach_evidence(run:, expected_revision:, item_id:, kind:, value:)
|
|
104
|
+
Runs.attach_evidence(run: run, expected_revision: expected_revision,
|
|
105
|
+
item_id: item_id, kind: kind, value: value)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def close_run(run:, expected_revision:, outcome:)
|
|
109
|
+
Runs.close(run: run, expected_revision: expected_revision, outcome: outcome)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def last_run(target:) = Runs.last(target: target)
|
|
113
|
+
|
|
114
|
+
def resolve(target:) = Operations.resolve(target: target)
|
|
115
|
+
|
|
116
|
+
def customize(target:, expected_revision:, topic:, description:, checklist:)
|
|
117
|
+
Operations.customize(target: target, expected_revision: expected_revision,
|
|
118
|
+
topic: topic, description: description, checklist: checklist)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def add_item(target:, expected_revision:, text:, required: true)
|
|
122
|
+
Operations.add_item(target: target, expected_revision: expected_revision,
|
|
123
|
+
text: text, required: required)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def remove_item(target:, expected_revision:, item_id:)
|
|
127
|
+
Operations.remove_item(target: target, expected_revision: expected_revision, item_id: item_id)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def check_item(target:, expected_revision:, item_id:, done:)
|
|
131
|
+
Operations.check_item(target: target, expected_revision: expected_revision,
|
|
132
|
+
item_id: item_id, done: done)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def reset(target:, expected_revision:)
|
|
136
|
+
Operations.reset(target: target, expected_revision: expected_revision)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Call from the host's after-commit when a subject is destroyed. See
|
|
140
|
+
# Operations.forget_subject — runs survive on purpose.
|
|
141
|
+
def forget_subject(subject) = Operations.forget_subject(subject)
|
|
142
|
+
end
|
|
143
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: jazari
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Nauman Tariq
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: activerecord
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '7.1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '7.1'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: activesupport
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '7.1'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '7.1'
|
|
40
|
+
description: A procedure you can call instead of a document you hope someone reads.
|
|
41
|
+
Recipes as data, per-subject overrides, stable queue names for rituals that outlive
|
|
42
|
+
any record, and per-run evidence so "did last night's run complete?" is a query.
|
|
43
|
+
email:
|
|
44
|
+
- 90499+nauman@users.noreply.github.com
|
|
45
|
+
executables: []
|
|
46
|
+
extensions: []
|
|
47
|
+
extra_rdoc_files: []
|
|
48
|
+
files:
|
|
49
|
+
- CHANGELOG.md
|
|
50
|
+
- LICENSE
|
|
51
|
+
- README.md
|
|
52
|
+
- app/models/jazari/anchor.rb
|
|
53
|
+
- app/models/jazari/application_record.rb
|
|
54
|
+
- app/models/jazari/recipe_record.rb
|
|
55
|
+
- app/models/jazari/run.rb
|
|
56
|
+
- app/models/jazari/runbook.rb
|
|
57
|
+
- lib/generators/jazari/install/install_generator.rb
|
|
58
|
+
- lib/generators/jazari/install/templates/create_jazari_tables.rb
|
|
59
|
+
- lib/jazari.rb
|
|
60
|
+
- lib/jazari/anchors.rb
|
|
61
|
+
- lib/jazari/checklist.rb
|
|
62
|
+
- lib/jazari/errors.rb
|
|
63
|
+
- lib/jazari/mcp/handler.rb
|
|
64
|
+
- lib/jazari/operations.rb
|
|
65
|
+
- lib/jazari/railtie.rb
|
|
66
|
+
- lib/jazari/recipe.rb
|
|
67
|
+
- lib/jazari/recipe_registry.rb
|
|
68
|
+
- lib/jazari/resolved_runbook.rb
|
|
69
|
+
- lib/jazari/runs.rb
|
|
70
|
+
- lib/jazari/targets.rb
|
|
71
|
+
- lib/jazari/version.rb
|
|
72
|
+
homepage: https://github.com/nauman/jazari
|
|
73
|
+
licenses:
|
|
74
|
+
- MIT
|
|
75
|
+
metadata:
|
|
76
|
+
homepage_uri: https://github.com/nauman/jazari
|
|
77
|
+
source_code_uri: https://github.com/nauman/jazari
|
|
78
|
+
changelog_uri: https://github.com/nauman/jazari/blob/main/CHANGELOG.md
|
|
79
|
+
rdoc_options: []
|
|
80
|
+
require_paths:
|
|
81
|
+
- lib
|
|
82
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
83
|
+
requirements:
|
|
84
|
+
- - ">="
|
|
85
|
+
- !ruby/object:Gem::Version
|
|
86
|
+
version: '3.2'
|
|
87
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
88
|
+
requirements:
|
|
89
|
+
- - ">="
|
|
90
|
+
- !ruby/object:Gem::Version
|
|
91
|
+
version: '0'
|
|
92
|
+
requirements: []
|
|
93
|
+
rubygems_version: 3.6.9
|
|
94
|
+
specification_version: 4
|
|
95
|
+
summary: 'Addressable operating procedures: recipes, runbooks, queues, and per-run
|
|
96
|
+
evidence.'
|
|
97
|
+
test_files: []
|