From 6024ef71c3110d1c6a5d5d3bbc37617bdaba20ac Mon Sep 17 00:00:00 2001 From: Zsolt Tasnadi Date: Sun, 23 Aug 2026 09:01:34 +0200 Subject: [PATCH] Every CI action is a CRUD action, and the URLs do not move MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `CiController` had three actions named after verbs — `pipelines`, `status`, `trigger` — and built its JSON by hand in a private `pipeline_json`, in a codebase where everything else is serialized by Blueprinter. Triggering a build is the creation of a run, so it is a resource of its own: GET /api/ci/pipelines -> Api::Ci::PipelinesController#index GET /api/ci/pipelines/:id/status -> Api::Ci::PipelinesController#show POST /api/ci/pipelines/:id/trigger -> Api::Ci::PipelineRunsController#create The addresses are untouched on purpose: they are what the CI and the scripts already call, and renaming them would be a breaking change for a version that does not need one. What changed is the shape behind them — three standard actions across two controllers, `require_ci!` and the pipeline scope in a shared base, and a `PipelineSerializer` that carries the fields the endpoint already answered with, `repo_id` and `woodpecker_repo_id` included (the duplicate stays: a client may read either, and ci_controller_spec asserts they agree). `/api/software/highlighted` answers with `#show` now rather than `#index`: it returns one title, and the action name should say so. Same address, same body. Co-Authored-By: Claude Opus 5 (1M context) --- .../warp_engine/api/ci/base_controller.rb | 19 +++++ .../api/ci/pipeline_runs_controller.rb | 31 ++++++++ .../api/ci/pipelines_controller.rb | 40 ++++++++++ .../warp_engine/api/ci_controller.rb | 75 ------------------- .../api/software_highlighted_controller.rb | 6 +- .../warp_engine/pipeline_serializer.rb | 15 ++++ config/routes.rb | 8 +- 7 files changed, 112 insertions(+), 82 deletions(-) create mode 100644 app/controllers/warp_engine/api/ci/base_controller.rb create mode 100644 app/controllers/warp_engine/api/ci/pipeline_runs_controller.rb create mode 100644 app/controllers/warp_engine/api/ci/pipelines_controller.rb delete mode 100644 app/controllers/warp_engine/api/ci_controller.rb create mode 100644 app/serializers/warp_engine/pipeline_serializer.rb diff --git a/app/controllers/warp_engine/api/ci/base_controller.rb b/app/controllers/warp_engine/api/ci/base_controller.rb new file mode 100644 index 0000000..fc4ace5 --- /dev/null +++ b/app/controllers/warp_engine/api/ci/base_controller.rb @@ -0,0 +1,19 @@ +module WarpEngine + module Api + module Ci + class BaseController < WarpEngine::ApiController + before_action :require_ci! + + private + + def require_ci! + return if WarpEngine.ci.configured? + + render json: { error: "CI not configured" }, status: :service_unavailable + end + + def pipeline_scope = WarpEngine::Pipeline.all + end + end + end +end diff --git a/app/controllers/warp_engine/api/ci/pipeline_runs_controller.rb b/app/controllers/warp_engine/api/ci/pipeline_runs_controller.rb new file mode 100644 index 0000000..f1ef9de --- /dev/null +++ b/app/controllers/warp_engine/api/ci/pipeline_runs_controller.rb @@ -0,0 +1,31 @@ +module WarpEngine + module Api + module Ci + class PipelineRunsController < BaseController + include WarpEngine::UpdateAuthentication + + resource_description do + short "CI pipeline runs" + end + + api :POST, "/api/ci/pipelines/:id/trigger", "Trigger a pipeline" + header "X-Update-Secret", "Shared secret or application token (update scope)", required: true + param :id, :number, required: true, desc: "Pipeline id" + 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: "No CI provider configured" + def create + unless update_authorized?(required_scope: WarpEngine::ApplicationToken::UPDATE_SCOPE) + return render json: { error: "Unauthorized" }, status: :unauthorized + end + + pipeline = pipeline_scope.find(params[:id]) + result = WarpEngine::PipelineService.new.trigger(pipeline, branch: params[:branch].presence || "main") + + render json: { triggered: true, pipeline: result } + end + end + end + end +end diff --git a/app/controllers/warp_engine/api/ci/pipelines_controller.rb b/app/controllers/warp_engine/api/ci/pipelines_controller.rb new file mode 100644 index 0000000..45510a0 --- /dev/null +++ b/app/controllers/warp_engine/api/ci/pipelines_controller.rb @@ -0,0 +1,40 @@ +module WarpEngine + module Api + module Ci + class PipelinesController < BaseController + resource_description do + short "CI pipelines" + end + + api :GET, "/api/ci/pipelines", "List active pipelines" + returns code: 200, desc: "JSON array of tracked pipelines" + error code: 503, desc: "No CI provider configured" + def index + pipelines = pipeline_scope.active.includes(:software) + render json: WarpEngine::PipelineSerializer.render_as_hash(pipelines) + end + + 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: "No CI provider configured" + def show + pipeline = pipeline_scope.find(params[:id]) + + render json: { + pipeline: WarpEngine::PipelineSerializer.render_as_hash(pipeline), + latest_run: latest_run(pipeline) + } + end + + private + + def latest_run(pipeline) + WarpEngine::PipelineService.new.run(pipeline, "latest") + rescue WarpEngine::CI::Error + nil + end + end + end + end +end diff --git a/app/controllers/warp_engine/api/ci_controller.rb b/app/controllers/warp_engine/api/ci_controller.rb deleted file mode 100644 index c495010..0000000 --- a/app/controllers/warp_engine/api/ci_controller.rb +++ /dev/null @@ -1,75 +0,0 @@ -module WarpEngine - module Api - class CiController < ApiController - include UpdateAuthentication - - before_action :require_ci! - - resource_description do - short "CI pipeline management" - end - - api :GET, "/api/ci/pipelines", "List active pipelines" - returns code: 200, desc: "JSON array of tracked pipelines" - error code: 503, desc: "No CI provider configured" - def pipelines - records = Pipeline.active.includes(:software) - render json: records.map { |p| pipeline_json(p) } - end - - 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: "No CI provider configured" - def status - pipeline = Pipeline.find(params[:id]) - latest_run = begin - PipelineService.new.run(pipeline, "latest") - rescue CI::Error - nil - end - render json: { pipeline: pipeline_json(pipeline), latest_run: latest_run } - end - - api :POST, "/api/ci/pipelines/:id/trigger", "Trigger a pipeline" - header "X-Update-Secret", "Shared secret or application token (update scope)", required: true - param :id, :number, required: true, desc: "Pipeline id" - 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: "No CI provider configured" - def trigger - unless update_authorized?(required_scope: ApplicationToken::UPDATE_SCOPE) - return render json: { error: "Unauthorized" }, status: :unauthorized - end - - pipeline = Pipeline.find(params[:id]) - result = PipelineService.new.trigger(pipeline, branch: params[:branch] || "main") - render json: { triggered: true, pipeline: result } - end - - private - - def require_ci! - return if WarpEngine.ci.configured? - - render json: { error: "CI not configured" }, status: :service_unavailable - end - - def pipeline_json(pipeline) - { - id: pipeline.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, - active: pipeline.active, - software_name: pipeline.software&.name, - last_pipeline_status: pipeline.last_pipeline_status, - last_pipeline_at: pipeline.last_pipeline_at - } - end - end - end -end diff --git a/app/controllers/warp_engine/api/software_highlighted_controller.rb b/app/controllers/warp_engine/api/software_highlighted_controller.rb index 903c5f8..a90ef0b 100644 --- a/app/controllers/warp_engine/api/software_highlighted_controller.rb +++ b/app/controllers/warp_engine/api/software_highlighted_controller.rb @@ -14,8 +14,8 @@ module WarpEngine property :desc, String, desc: "Short description" property :story, String, desc: "Long description / story" property :license, String, desc: "License type" - property :platform, String, desc: "Platform (tic80, love, ebitengine, c64, godot, bevy, phaser)" - property :status, String, desc: "Status" + property :platform, String, desc: "Platform (one of WarpEngine::Platform::NAMES)" + property :status, String, desc: "Status (development, demo, released, archived)" property :highlighted, :boolean, desc: "Highlighted flag" property :imageUrl, String, desc: "Default image URL" end @@ -33,7 +33,7 @@ module WarpEngine property :totalDownloads, Integer, desc: "Total download count across all releases" end error code: 404, desc: "No highlighted software found" - def index + def show result = WarpEngine::SoftwareHighlightedService.new.index(subject: current_subject) if result render json: result diff --git a/app/serializers/warp_engine/pipeline_serializer.rb b/app/serializers/warp_engine/pipeline_serializer.rb new file mode 100644 index 0000000..06bdb61 --- /dev/null +++ b/app/serializers/warp_engine/pipeline_serializer.rb @@ -0,0 +1,15 @@ +module WarpEngine + class PipelineSerializer < Blueprinter::Base + field :id + field :repo_owner + field :repo_name + field :platform + field :active + field :last_pipeline_status + field :last_pipeline_at + + field(:repo_id) { |pipeline| pipeline.remote_repo_id } + field(:woodpecker_repo_id) { |pipeline| pipeline.remote_repo_id } + field(:software_name) { |pipeline| pipeline.software&.name } + end +end diff --git a/config/routes.rb b/config/routes.rb index b8990bd..c056e7b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -9,14 +9,14 @@ WarpEngine::Engine.routes.draw do end get "software", to: "software#index" - get "software/highlighted", to: "software_highlighted#index" + get "software/highlighted", to: "software_highlighted#show" get "download", to: "downloads#show" get "builds", to: "builds#index" get "softwares/:name/builds", to: "software_builds#show" - get "ci/pipelines", to: "ci#pipelines" - get "ci/pipelines/:id/status", to: "ci#status" - post "ci/pipelines/:id/trigger", to: "ci#trigger" + get "ci/pipelines", to: "ci/pipelines#index" + get "ci/pipelines/:id/status", to: "ci/pipelines#show" + post "ci/pipelines/:id/trigger", to: "ci/pipeline_runs#create" end post "build/upload", to: "build/uploads#create"