Every CI action is a CRUD action, and the URLs do not move
`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) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
|
||||||
@@ -14,8 +14,8 @@ module WarpEngine
|
|||||||
property :desc, String, desc: "Short description"
|
property :desc, String, desc: "Short description"
|
||||||
property :story, String, desc: "Long description / story"
|
property :story, String, desc: "Long description / story"
|
||||||
property :license, String, desc: "License type"
|
property :license, String, desc: "License type"
|
||||||
property :platform, String, desc: "Platform (tic80, love, ebitengine, c64, godot, bevy, phaser)"
|
property :platform, String, desc: "Platform (one of WarpEngine::Platform::NAMES)"
|
||||||
property :status, String, desc: "Status"
|
property :status, String, desc: "Status (development, demo, released, archived)"
|
||||||
property :highlighted, :boolean, desc: "Highlighted flag"
|
property :highlighted, :boolean, desc: "Highlighted flag"
|
||||||
property :imageUrl, String, desc: "Default image URL"
|
property :imageUrl, String, desc: "Default image URL"
|
||||||
end
|
end
|
||||||
@@ -33,7 +33,7 @@ module WarpEngine
|
|||||||
property :totalDownloads, Integer, desc: "Total download count across all releases"
|
property :totalDownloads, Integer, desc: "Total download count across all releases"
|
||||||
end
|
end
|
||||||
error code: 404, desc: "No highlighted software found"
|
error code: 404, desc: "No highlighted software found"
|
||||||
def index
|
def show
|
||||||
result = WarpEngine::SoftwareHighlightedService.new.index(subject: current_subject)
|
result = WarpEngine::SoftwareHighlightedService.new.index(subject: current_subject)
|
||||||
if result
|
if result
|
||||||
render json: result
|
render json: result
|
||||||
|
|||||||
@@ -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
|
||||||
+4
-4
@@ -9,14 +9,14 @@ WarpEngine::Engine.routes.draw do
|
|||||||
end
|
end
|
||||||
|
|
||||||
get "software", to: "software#index"
|
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 "download", to: "downloads#show"
|
||||||
get "builds", to: "builds#index"
|
get "builds", to: "builds#index"
|
||||||
get "softwares/:name/builds", to: "software_builds#show"
|
get "softwares/:name/builds", to: "software_builds#show"
|
||||||
|
|
||||||
get "ci/pipelines", to: "ci#pipelines"
|
get "ci/pipelines", to: "ci/pipelines#index"
|
||||||
get "ci/pipelines/:id/status", to: "ci#status"
|
get "ci/pipelines/:id/status", to: "ci/pipelines#show"
|
||||||
post "ci/pipelines/:id/trigger", to: "ci#trigger"
|
post "ci/pipelines/:id/trigger", to: "ci/pipeline_runs#create"
|
||||||
end
|
end
|
||||||
|
|
||||||
post "build/upload", to: "build/uploads#create"
|
post "build/upload", to: "build/uploads#create"
|
||||||
|
|||||||
Reference in New Issue
Block a user