WarpEngine 0.7.0: a képtár és a CI is a hoszté, adapteren keresztül

Két dolog volt beépítve az engine-be, ami nem az övé.

A **képtár** eddig `WarpEngine::Image` volt, pedig a modell teljesen általános:
a teletypegames-ben a tagok arcképét is ez hordozza, nem csak a katalógus
borítóit. Az `Image` modell, a feltöltött fájlok, az `/api/image/:id` végpont
és az admin oldal ezért átkerült a hosztba, az engine pedig adapteren szól
hozzá (`WarpEngine::Images`): `url_for` adja a katalógus JSON `imageUrl`-jét,
`select_options` a software-form képválasztóját, `build_from_upload` a
"tölts fel új képet" ágat. Az alapértelmezés az `Image` osztály, tehát a
default útvonal bitre a régi. A `SoftwareImage` (a katalógus-kapcsolat)
maradt az engine-ben, és **az `images` tábla nem mozdult**: az engine csak
abbahagyta a létrehozását, a generátor írja meg hoszt-kódként.

A **CI** eddig végig Woodpecker volt: kliens, aláírás-ellenőrzés,
pipeline-receptek, repo-szinkron, secret-kiosztás. Mindez egy adapter mögé
került (`WarpEngine.ci`), a Woodpecker-implementáció pedig az engine-ben
maradt `WarpEngine::CI::Woodpecker` néven — kliens, adapter, httpsig-ellenőrző
és a platformonkénti pipeline-receptek, mert a YAML-dialektus a szolgáltatóé.
Az engine saját kódja már nem nevez szolgáltatót: `CI::Repo` és `CI::Run`
értékeket kap, `CI::ConnectionError`/`ApiError`/`NotConfigured` hibákat dob, a
`Pipeline` pedig `remote_repo_id`-t ad a történelmi `woodpecker_repo_id`
kolumna fölött (a tábla itt sem mozdult). `c.ci_adapter = :none` azt jelenti,
hogy ez a hoszt nem buildel: az `/api/ci/*` 503, a `/build/config` elutasít,
az admin akciók elbújnak.

Mindkét seam a hoszt initializerében van kimondva, nem alapértelmezésre
hagyva — a hoszt megnevezi, mi a képtára és mi a CI-ja.

Törés a 0.6-hoz képest: `image_container_path`, `image_owners`,
`ci_platforms`, `ci_update_server`, `ci_extension_public_key(_url)`,
`woodpecker_url`, `woodpecker_api_token`, `woodpecker_repo_owner` és a
`WarpEngine.woodpecker_configured?` megszűnt; a helyük `c.image_class_name` /
`c.image_adapter` és `c.ci_adapter`. Az `/api/ci/*` `latest_run`/`trigger`
válasza a normalizált `CI::Run` alakot adja (number, status, branch, message,
createdAt, url), a `pipelines` lista pedig `repo_id`-t is közöl a megtartott
`woodpecker_repo_id` mellett.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-23 00:56:01 +02:00
co-authored by Claude Opus 5
parent a0fbf1e2b4
commit 19d142ef64
62 changed files with 1391 additions and 793 deletions
@@ -3,7 +3,7 @@ module WarpEngine
class CiController < ApiController
include UpdateAuthentication
before_action :require_woodpecker!
before_action :require_ci!
resource_description do
short "CI pipeline management"
@@ -11,7 +11,7 @@ module WarpEngine
api :GET, "/api/ci/pipelines", "List active pipelines"
returns code: 200, desc: "JSON array of tracked pipelines"
error code: 503, desc: "Woodpecker not configured"
error code: 503, desc: "No CI provider configured"
def pipelines
records = Pipeline.active.includes(:software)
render json: records.map { |p| pipeline_json(p) }
@@ -20,12 +20,12 @@ module WarpEngine
api :GET, "/api/ci/pipelines/:id/status", "Get a pipeline with its latest run"
param :id, :number, required: true, desc: "Pipeline id"
returns code: 200, desc: "JSON with pipeline and latest run data"
error code: 503, desc: "Woodpecker not configured"
error code: 503, desc: "No CI provider configured"
def status
pipeline = Pipeline.find(params[:id])
latest_run = begin
PipelineService.new.pipeline_detail(pipeline, "latest")
rescue WoodpeckerClient::ApiError
PipelineService.new.run(pipeline, "latest")
rescue CI::Error
nil
end
render json: { pipeline: pipeline_json(pipeline), latest_run: latest_run }
@@ -37,7 +37,7 @@ module WarpEngine
param :branch, String, required: false, desc: "Branch to build (default: main)"
returns code: 200, desc: "JSON with triggered run data"
error code: 401, desc: "Invalid secret"
error code: 503, desc: "Woodpecker not configured"
error code: 503, desc: "No CI provider configured"
def trigger
unless update_authorized?(required_scope: ApplicationToken::UPDATE_SCOPE)
return render json: { error: "Unauthorized" }, status: :unauthorized
@@ -50,16 +50,17 @@ module WarpEngine
private
def require_woodpecker!
return if WarpEngine.woodpecker_configured?
def require_ci!
return if WarpEngine.ci.configured?
render json: { error: "Woodpecker not configured" }, status: :service_unavailable
render json: { error: "CI not configured" }, status: :service_unavailable
end
def pipeline_json(pipeline)
{
id: pipeline.id,
woodpecker_repo_id: pipeline.woodpecker_repo_id,
repo_id: pipeline.remote_repo_id,
woodpecker_repo_id: pipeline.remote_repo_id,
repo_owner: pipeline.repo_owner,
repo_name: pipeline.repo_name,
platform: pipeline.platform,
@@ -1,17 +0,0 @@
module WarpEngine
class Api::ImagesController < ApiController
resource_description do
short "Images"
formats [ "binary" ]
end
api :GET, "/api/image/:id", "Get image by ID"
param :id, :number, required: true, desc: "Image ID"
returns code: 200, desc: "Image binary data"
error code: 404, desc: "Image not found"
def show
image = WarpEngine::ImageService.new.show(WarpEngine::ImageShowInputDto.new(id: params[:id]))
send_file image.file_path, type: image.content_type, disposition: "inline"
end
end
end
@@ -3,76 +3,60 @@ module WarpEngine
class ConfigsController < ApiController
resource_description do
short "Woodpecker CI pipeline configs"
short "CI pipeline configs"
end
api :GET, "/build/config", "Preview the generated pipeline config for a platform"
param :platform, String, required: true, desc: "Platform (a configured ci_platforms key, e.g. tic80)"
param :platform, String, required: true, desc: "Platform (a platform the CI adapter serves, e.g. tic80)"
param :name, String, required: false, desc: "Software name substituted into the pipeline (default: example)"
returns code: 200, desc: "Pipeline YAML (text/yaml)"
error code: 404, desc: "Unknown platform"
def show
yaml = render_config(platform: params[:platform], name: params[:name].presence || "example")
return render json: { error: "Unknown platform" }, status: :not_found if yaml.nil?
config = render_config(platform: params[:platform], name: params[:name].presence || "example")
return render json: { error: "Unknown platform" }, status: :not_found if config.nil?
render plain: yaml, content_type: "text/yaml"
render plain: config, content_type: "text/yaml"
end
api :POST, "/build/config", "Woodpecker configuration extension endpoint"
api :POST, "/build/config", "CI configuration extension endpoint"
description <<~DESC
Called by the Woodpecker server on every pipeline start (httpsig-signed request).
If the repo's .woodpecker.yaml is a marker (has a `platform:` key), responds with
Called by the CI server on every pipeline start (a signed request the CI adapter
verifies). If the repo's config is a marker (has a `platform:` key), responds with
the generated pipeline; otherwise responds 204 so the repo's own config runs.
DESC
returns code: 200, desc: %(JSON: {"configs": [{"name": ..., "data": "<pipeline YAML>"}]})
returns code: 200, desc: %(JSON: the adapter's config response — for Woodpecker {"configs": [{"name": ..., "data": "<pipeline YAML>"}]})
returns code: 204, desc: "Not a marker config — keep the repo's own configuration"
error code: 403, desc: "Missing or invalid request signature"
error code: 422, desc: "Marker requests an unknown platform"
def create
unless WarpEngine::CiSignatureVerifier.new(request).valid?
unless ci.verify_config_request(request)
return render json: { error: "Invalid signature" }, status: :forbidden
end
marker = find_marker
marker = ci.config_marker(params)
return head :no_content if marker.nil?
platform = marker["platform"].to_s
name = marker["name"].presence || repo_name
yaml = render_config(platform: platform, name: name)
if yaml.nil?
return render json: { error: "Unknown platform: #{platform}" }, status: :unprocessable_entity
config = render_config(platform: marker[:platform], name: marker[:name])
if config.nil?
return render json: { error: "Unknown platform: #{marker[:platform]}" }, status: :unprocessable_entity
end
render json: { configs: [ { name: platform, data: yaml } ] }
render json: ci.config_response(platform: marker[:platform], config: config)
end
private
def ci
WarpEngine.ci
end
def render_config(platform:, name:)
WarpEngine::CiConfigService.new.render(
ci.pipeline_config(
platform: platform,
name: name,
update_server: WarpEngine.config.ci_update_server.presence || request.base_url
update_server: ci.update_server.presence || request.base_url
)
end
def find_marker
configs = params[:configuration].presence || params[:configs].presence || []
configs.each do |config|
data = config[:data].to_s
parsed = begin
YAML.safe_load(data)
rescue Psych::Exception
nil
end
return parsed if parsed.is_a?(Hash) && parsed.key?("platform")
end
nil
end
def repo_name
params.dig(:repo, :name).to_s
end
end
end
end