Class: Ecoportal::API::GraphQL::Diff::Deploy

Inherits:
Object
  • Object
show all
Defined in:
lib/ecoportal/api/graphql/diff/deploy.rb

Overview

Deploy orchestration: turns a diff into the replayable, ordered WorkflowCommand batch that executeWorkflowCommands consumes — plus the honest list of changes that could NOT be synthesised (needs-human), so a review step can gate the apply.

Two entry points:

# self-version replay (same object, real ids) — the "commit" replayed onto itself/a clone plan = Deploy.from_versions(before_doc, after_doc)

# cross-object deploy (UAT delta -> a real PROD target): the diff gives the delta, but the # move targets (section/stage) must resolve against the PROD doc's ids. plan = Deploy.from_versions(uat_v1, uat_v2, target_doc: prod_doc)

plan.commands # => ordered Array of built command hashes, ready to send plan.unsupported # => [Change, ...] needing human handling (never guessed) plan.changelog # => human one-liners for a ticket / review checklist plan.execute!(page) # => sends via the given executor (page.execute_workflow_commands)

SAFETY — Deploy is inert until execute! is called with an explicit executor. It never applies anything on its own, and it surfaces unsupported so a human gates the deploy.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(diff, resolver: nil, thread_placeholders: true) ⇒ Deploy

Returns a new instance of Deploy.

Parameters:

  • diff (VersionDiff)
  • resolver (#resolve, nil) (defaults to: nil)

    maps move targets (stage name / section heading) to ids.

  • thread_placeholders (Boolean) (defaults to: true)

    thread client-chosen placeholderIds so that an add-then-reference sequence (e.g. add a Select field, then add its options) stays self-consistent within one executeWorkflowCommands call. ON by default for deploy — the whole batch is applied to a target where the source ids are not valid.



34
35
36
37
38
39
# File 'lib/ecoportal/api/graphql/diff/deploy.rb', line 34

def initialize(diff, resolver: nil, thread_placeholders: true)
  @diff        = diff
  @synthesizer = CommandSynthesizer.new(
    diff.changes, resolver: resolver, thread_placeholders: thread_placeholders
  )
end

Instance Attribute Details

#diffObject (readonly)

Returns the value of attribute diff.



26
27
28
# File 'lib/ecoportal/api/graphql/diff/deploy.rb', line 26

def diff
  @diff
end

#synthesizerObject (readonly)

Returns the value of attribute synthesizer.



26
27
28
# File 'lib/ecoportal/api/graphql/diff/deploy.rb', line 26

def synthesizer
  @synthesizer
end

Class Method Details

.from_cross_object(source_doc, target_doc, engine: nil, resolver: nil, thread_placeholders: true, strategy: Strategy.new(pairing: :assisted, scope: :data_migration, intent: :deploy)) ⇒ Object

Build a plan for a CROSS-OBJECT deploy (UAT<->PROD, page<->template): the two docs do not share ids, so fields are PAIRED (equivalence) before the delta is emitted. The pairing (accepted/ambiguous/unmatched) is exposed via #pairing so a human can adjudicate the unresolved set before applying. engine: lets the caller pass a Pairing::Engine wired to a Ledger (learns over time); strategy: defaults to the assisted/data-migration mode.

plan = Deploy.from_cross_object(uat_doc, prod_doc, engine: engine) plan.pairing.ambiguous # review before deploy plan.commands # the paired delta as WorkflowCommands



64
65
66
67
68
69
70
# File 'lib/ecoportal/api/graphql/diff/deploy.rb', line 64

def self.from_cross_object(source_doc, target_doc, engine: nil, resolver: nil,
                           thread_placeholders: true,
                           strategy: Strategy.new(pairing: :assisted, scope: :data_migration, intent: :deploy))
  resolver ||= (IdResolver.from_doc(target_doc) if target_doc)
  diff = CrossObjectDiff.new(source_doc, target_doc, engine: engine, strategy: strategy)
  new(diff, resolver: resolver, thread_placeholders: thread_placeholders)
end

.from_versions(before_doc, after_doc, target_doc: nil, resolver: nil, thread_placeholders: true, strategy: Strategy.default) ⇒ Object

Build a plan from two snapshots. When target_doc is given (cross-object deploy), a resolver is derived from it so field/section moves can address the target's real ids. An explicit resolver: overrides the derived one.

strategy: selects the diff modality (scope / move-sensitivity / intent). It defaults to the self-version structural changelog strategy — the historical behaviour. When the strategy's intent is :deploy (or unspecified) placeholder threading stays ON.



48
49
50
51
52
53
# File 'lib/ecoportal/api/graphql/diff/deploy.rb', line 48

def self.from_versions(before_doc, after_doc, target_doc: nil, resolver: nil,
                       thread_placeholders: true, strategy: Strategy.default)
  resolver ||= (IdResolver.from_doc(target_doc) if target_doc)
  diff = VersionDiff.new(before_doc, after_doc, strategy: strategy)
  new(diff, resolver: resolver, thread_placeholders: thread_placeholders)
end

Instance Method Details

#changelogObject



88
89
90
# File 'lib/ecoportal/api/graphql/diff/deploy.rb', line 88

def changelog
  @diff.changelog
end

#commandsObject

Ordered, dependency-safe Array of built command hashes ready for executeWorkflowCommands.



79
80
81
# File 'lib/ecoportal/api/graphql/diff/deploy.rb', line 79

def commands
  @synthesizer.commands
end

#execute!(executor, allow_partial: false) ⇒ Object

Apply the batch. executor must respond to execute_workflow_commands(commands) (the gem's page/template mutation facade). Raises unless the plan is fully supported — the caller must review/clear unsupported first — unless allow_partial: true.

Raises:

  • (ArgumentError)


112
113
114
115
116
# File 'lib/ecoportal/api/graphql/diff/deploy.rb', line 112

def execute!(executor, allow_partial: false)
  raise ArgumentError, "#{unsupported.size} unsupported change(s); review before deploy" unless fully_supported? || allow_partial

  executor.execute_workflow_commands(commands)
end

#fully_supported?Boolean

True when every detected change was synthesised into a command (nothing needs a human).

Returns:

  • (Boolean)


101
102
103
# File 'lib/ecoportal/api/graphql/diff/deploy.rb', line 101

def fully_supported?
  unsupported.empty?
end

#pairingObject

The pairing result when this plan is cross-object (nil for a self-version plan). Lets a caller inspect/adjudicate ambiguous + unmatched pairings before execute!.



74
75
76
# File 'lib/ecoportal/api/graphql/diff/deploy.rb', line 74

def pairing
  @diff.respond_to?(:pairing) ? @diff.pairing : nil
end

#summaryObject



92
93
94
95
96
97
98
# File 'lib/ecoportal/api/graphql/diff/deploy.rb', line 92

def summary
  {
    changes:     @diff.summary,
    commands:    commands.size,
    unsupported: unsupported.size
  }
end

#to_hObject



105
106
107
# File 'lib/ecoportal/api/graphql/diff/deploy.rb', line 105

def to_h
  { summary: summary, commands: commands, unsupported: unsupported.map(&:to_h) }
end

#unsupportedObject

Changes with no faithful command — surfaced for human review, never guessed.



84
85
86
# File 'lib/ecoportal/api/graphql/diff/deploy.rb', line 84

def unsupported
  @synthesizer.unsupported
end