A version header on every WarpEngine response, and a movable pipeline link

Every WarpEngine API response now carries `WarpEngine-Version`, so a client can branch on
the engine's age without a round trip to ask. Set in a before_action rather than after:
`rescue_from` never reaches an after_action, and a client needs the version most when
something came back wrong. The name lives in `WarpEngine::VERSION_HEADER`. The host's own
endpoints — the store registry — do not carry it, because they are not the engine.

A software has one pipeline, and the newest assignment now wins. Two pipelines pointing at
the same software was not an error the database caught; it was a link that silently did
nothing, with the software still showing whichever row came first. Assigning a software
another pipeline holds therefore moves it, the admin says which pipeline it was taken
from, and `Pipeline#software_taken_from` carries that for anything else that cares.
Deliberately a callback and not a unique index: rows here are soft-deleted, and a unique
index counts deleted rows, so a pipeline removed last year would block its software from
ever being linked again.

The engine is 0.4.0. The site's /stores page and its screenshot follow the client's new
name, and the shot is a fresh one showing the greyed-out titles the client now lists.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-18 19:46:29 +02:00
co-authored by Claude Opus 5
parent 9b5c05e647
commit b652615e32
7 changed files with 150 additions and 2 deletions
+27
View File
@@ -8,8 +8,25 @@ module WarpEngine
belongs_to :software, class_name: "WarpEngine::Software", optional: true
# Pipelines this record took the software from during the last save, by full name.
# The admin says so out loud: a silent reassignment is what made the old behaviour
# confusing in the first place.
attr_reader :software_taken_from
default_scope { where(deleted_at: nil) }
# One pipeline per software, and the newest assignment wins.
#
# `Software#pipeline` is a `has_one`, so two pipelines pointing at the same software
# is not an error — it is worse than one: the software keeps showing whichever row
# comes first, and assigning it elsewhere looks like it did nothing. Rather than
# refusing the assignment, the link moves: whoever held that software lets go of it.
#
# Deliberately a callback and not a unique index. Rows here are soft-deleted, and a
# unique index counts deleted rows too, so a pipeline someone removed last year would
# block the software from ever being linked again.
before_save :claim_software_from_other_pipelines, if: :will_save_change_to_software_id?
validates :woodpecker_repo_id, presence: true, uniqueness: true
validates :repo_owner, presence: true
validates :repo_name, presence: true
@@ -31,6 +48,16 @@ module WarpEngine
%w[software]
end
private
def claim_software_from_other_pipelines
return if software_id.blank?
others = Pipeline.where(software_id: software_id).where.not(id: id)
@software_taken_from = others.map(&:full_name)
others.update_all(software_id: nil, updated_at: Time.current)
end
ActiveSupport.run_load_hooks(:warp_engine_pipeline, self)
end
end