Class: CollavreLinear::CreativeExporter
- Inherits:
-
Object
- Object
- CollavreLinear::CreativeExporter
- Defined in:
- app/services/collavre_linear/creative_exporter.rb
Overview
Syncs a single Collavre::Creative to Linear as an issue.
Usage:
CollavreLinear::CreativeExporter.new(creative).sync!
Resolves the governing ProjectLink from the creative itself or its nearest ancestor. Creates a new Linear issue and IssueLink on first run; updates the existing issue on subsequent runs. Skips the network call when the content hash is unchanged (dirty-tracking). Raises Client::Error on network failure so the caller (OutboundSyncJob) can retry.
Project-root mapping: the ProjectLink-root Creative maps to the Linear PROJECT itself, NOT an issue. Its direct children become the project's TOP-LEVEL issues (parentId nil); deeper descendants nest as sub-issues. Exporting the root as an issue would consume the project's single top-level slot and flatten every sibling under it, so sync! no-ops for the root. Inbound mirrors this: a top-level Linear issue is imported as a direct child of the root Creative (see InboundApplier#resolve_create_parent).
Defined Under Namespace
Classes: CreativeAdapter, ParentNotExportedError
Class Method Summary collapse
-
.content_hash_for(creative) ⇒ Object
Compute the outbound content hash for a Creative's current state.
-
.hash_attrs(attrs, parent_linear_issue_id) ⇒ Object
SHA-256 of the sorted, serialised mapped attrs plus the parent's linear issue id (stable key order).
-
.parent_linear_issue_id_for(creative) ⇒ Object
Resolve the parent Creative's linked linear_issue_id (or nil when the parent is absent or not itself linked to a Linear issue).
Instance Method Summary collapse
-
#initialize(creative) ⇒ CreativeExporter
constructor
A new instance of CreativeExporter.
- #sync! ⇒ Object
Constructor Details
#initialize(creative) ⇒ CreativeExporter
Returns a new instance of CreativeExporter.
75 76 77 |
# File 'app/services/collavre_linear/creative_exporter.rb', line 75 def initialize(creative) @creative = creative end |
Class Method Details
.content_hash_for(creative) ⇒ Object
Compute the outbound content hash for a Creative's current state. Shared with the inbound applier so it can advance IssueLink#content_hash after applying a remote update, keeping the exporter's dirty-check consistent.
The hash folds in the parent's linear_issue_id so that a pure reparent (content fields unchanged) still changes the hash and drives update_issue!, which pushes the new parentId to Linear. Otherwise Collavre and Linear hierarchies would diverge after a move.
46 47 48 49 50 51 52 53 54 55 56 |
# File 'app/services/collavre_linear/creative_exporter.rb', line 46 def self.content_hash_for(creative) attrs = FieldMapper.creative_to_issue_attrs( CreativeAdapter.new( title: creative.creative_snippet, description: creative.description, sequence: creative.sequence, data: creative.data ) ) hash_attrs(attrs, parent_linear_issue_id_for(creative)) end |
.hash_attrs(attrs, parent_linear_issue_id) ⇒ Object
SHA-256 of the sorted, serialised mapped attrs plus the parent's linear issue id (stable key order). Single source of truth so inbound + outbound and the class/instance paths all agree.
70 71 72 73 |
# File 'app/services/collavre_linear/creative_exporter.rb', line 70 def self.hash_attrs(attrs, parent_linear_issue_id) payload = attrs.merge(_parent_linear_issue_id: parent_linear_issue_id) Digest::SHA256.hexdigest(payload.sort.to_h.to_json) end |
.parent_linear_issue_id_for(creative) ⇒ Object
Resolve the parent Creative's linked linear_issue_id (or nil when the parent is absent or not itself linked to a Linear issue).
60 61 62 63 64 65 |
# File 'app/services/collavre_linear/creative_exporter.rb', line 60 def self.parent_linear_issue_id_for(creative) parent = creative.parent return nil unless parent parent.linear_issue_links.first&.linear_issue_id end |
Instance Method Details
#sync! ⇒ Object
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 |
# File 'app/services/collavre_linear/creative_exporter.rb', line 79 def sync! project_link = resolve_project_link return unless project_link # The ProjectLink-root Creative IS the project, not an issue — skip it. # (Its children export as the project's top-level issues; see class docs.) return if project_link.creative_id == @creative.id account = project_link.account client = Client.new(account) attrs = FieldMapper.creative_to_issue_attrs(adapt(@creative)) parent_id = parent_linear_issue_id hash = compute_content_hash(attrs, parent_id) issue_link = @creative.linear_issue_links.first # Cross-project move: the creative was reparented under a DIFFERENT linked # root than the one its existing issue belongs to. resolve_project_link now # returns the new project, so updating would push the OLD project's issue # with the NEW project's account/client (wrong project) and diverge the tree # from Linear. It cannot be auto-applied — re-homing the issue (and its # linked sub-issues, which move with it in Linear) into the new project is # ambiguous. Halt + surface as conflict (mirrors the inbound cross-project # handling); actively migrating/recreating is a product decision. Checked # before the parent-export guard so a cross-root move never defers. if issue_link && issue_link.project_link_id != project_link.id return mark_cross_project_conflict!(issue_link) end # Ordering guard for BOTH create and update: if this creative's parent # belongs to the exported subtree but has not yet produced its Linear issue, # defer (the job re-enqueues on ParentNotExportedError once the parent lands). # - create: exporting now would make a top-level issue, flattening the tree. # - update: an already-linked creative moved under a NEWLY-created parent # can run before the parent's create job. Without waiting we'd send no # parentId, persist parent_issue_id for the still-nil parent, and the # later parent export would NOT re-enqueue us — leaving the Linear issue # under its old parent until a manual resync. raise ParentNotExportedError if parent_export_pending? if issue_link.nil? create_issue!(client, project_link, attrs, hash, parent_id) else update_issue!(client, issue_link, attrs, hash, parent_id) end end |