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:
@@ -10,11 +10,21 @@ RSpec.describe "Build configs endpoint", type: :request do
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
allow(WarpEngine.config).to receive(:ci_platforms).and_return(ci_platforms)
|
||||
allow(WarpEngine.config).to receive(:ci_extension_public_key).and_return(signing_key.public_to_pem)
|
||||
def woodpecker(platforms: nil, public_key: :default)
|
||||
WarpEngine::CI::Woodpecker::Adapter.new(
|
||||
url: "https://ci.example.test",
|
||||
api_token: "wp-token",
|
||||
platforms: platforms.nil? ? ci_platforms : platforms,
|
||||
public_key: public_key == :default ? signing_key.public_to_pem : public_key
|
||||
)
|
||||
end
|
||||
|
||||
def serving(adapter)
|
||||
allow(WarpEngine).to receive(:ci).and_return(adapter)
|
||||
end
|
||||
|
||||
before { serving(woodpecker) }
|
||||
|
||||
def signed_headers(body, path: "/build/config", digest_body: nil)
|
||||
digest = "sha-256=:#{Digest::SHA256.base64digest(digest_body || body)}:"
|
||||
inner = %{("@request-target" "content-digest");created=#{Time.now.to_i};alg="ed25519"}
|
||||
@@ -69,7 +79,7 @@ RSpec.describe "Build configs endpoint", type: :request do
|
||||
end
|
||||
|
||||
it "returns 404 when the feature is not configured" do
|
||||
allow(WarpEngine.config).to receive(:ci_platforms).and_return({})
|
||||
serving(woodpecker(platforms: {}))
|
||||
|
||||
get "/build/config", params: { platform: "godot" }
|
||||
|
||||
@@ -146,7 +156,7 @@ RSpec.describe "Build configs endpoint", type: :request do
|
||||
payload = extension_payload("platform: godot\n")
|
||||
headers = signed_headers(payload)
|
||||
other_key = OpenSSL::PKey.generate_key("ed25519")
|
||||
allow(WarpEngine.config).to receive(:ci_extension_public_key).and_return(other_key.public_to_pem)
|
||||
serving(woodpecker(public_key: other_key.public_to_pem))
|
||||
|
||||
post "/build/config", params: payload, headers: headers
|
||||
|
||||
@@ -170,8 +180,7 @@ RSpec.describe "Build configs endpoint", type: :request do
|
||||
end
|
||||
|
||||
it "rejects every request when no public key is configured" do
|
||||
allow(WarpEngine.config).to receive(:ci_extension_public_key).and_return(nil)
|
||||
allow(WarpEngine.config).to receive(:ci_extension_public_key_url).and_return(nil)
|
||||
serving(woodpecker(public_key: nil))
|
||||
|
||||
payload = extension_payload("platform: godot\n")
|
||||
post "/build/config", params: payload, headers: signed_headers(payload)
|
||||
@@ -182,16 +191,17 @@ RSpec.describe "Build configs endpoint", type: :request do
|
||||
|
||||
describe "shipped templates" do
|
||||
it "renders every template to valid YAML with non-empty steps" do
|
||||
templates = Dir[WarpEngine::Engine.root.join("app/services/warp_engine/platforms/*/pipeline.yaml.erb")]
|
||||
templates = Dir[WarpEngine::CI::Woodpecker::PipelineConfig.templates_dir.join("*/pipeline.yaml.erb")]
|
||||
expect(templates).not_to be_empty
|
||||
|
||||
templates.each do |path|
|
||||
platform = File.basename(File.dirname(path))
|
||||
allow(WarpEngine.config).to receive(:ci_platforms).and_return(
|
||||
platform => { builder: "registry.example/builder:1", exporter: "registry.example/exporter:1" }
|
||||
config = WarpEngine::CI::Woodpecker::PipelineConfig.new(
|
||||
platforms: { platform => { builder: "registry.example/builder:1",
|
||||
exporter: "registry.example/exporter:1" } }
|
||||
)
|
||||
|
||||
yaml = WarpEngine::CiConfigService.new.render(
|
||||
yaml = config.render(
|
||||
platform: platform, name: "example", update_server: "https://games.example"
|
||||
)
|
||||
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe "CI API", type: :request do
|
||||
let(:ci) { instance_double(WarpEngine::CI::Woodpecker::Adapter, configured?: true, name: "Woodpecker") }
|
||||
|
||||
before do
|
||||
allow(WarpEngine.config).to receive(:woodpecker_url).and_return("https://ci.test")
|
||||
allow(WarpEngine.config).to receive(:woodpecker_api_token).and_return("wp-token")
|
||||
allow(WarpEngine).to receive(:ci).and_return(ci)
|
||||
allow(WarpEngine.config).to receive(:update_secret).and_return("s3cret")
|
||||
end
|
||||
|
||||
describe "GET /api/ci/pipelines" do
|
||||
it "returns active repos" do
|
||||
repo = create(:pipeline, repo_name: "mygame", platform: "tic80")
|
||||
create(:pipeline, repo_name: "mygame", platform: "tic80")
|
||||
|
||||
get "/api/ci/pipelines"
|
||||
|
||||
@@ -17,10 +18,11 @@ RSpec.describe "CI API", type: :request do
|
||||
json = JSON.parse(response.body)
|
||||
expect(json.size).to eq(1)
|
||||
expect(json.first["repo_name"]).to eq("mygame")
|
||||
expect(json.first["repo_id"]).to eq(json.first["woodpecker_repo_id"])
|
||||
end
|
||||
|
||||
it "returns 503 when woodpecker not configured" do
|
||||
allow(WarpEngine.config).to receive(:woodpecker_url).and_return(nil)
|
||||
it "returns 503 when no CI provider is configured" do
|
||||
allow(ci).to receive(:configured?).and_return(false)
|
||||
|
||||
get "/api/ci/pipelines"
|
||||
|
||||
@@ -29,18 +31,27 @@ RSpec.describe "CI API", type: :request do
|
||||
end
|
||||
|
||||
describe "GET /api/ci/pipelines/:id/status" do
|
||||
it "returns repo with pipeline status" do
|
||||
it "returns the repo with its latest run" do
|
||||
repo = create(:pipeline, repo_owner: "org", repo_name: "game")
|
||||
client = instance_double(WarpEngine::WoodpeckerClient)
|
||||
allow(WarpEngine::WoodpeckerClient).to receive(:new).and_return(client)
|
||||
allow(client).to receive(:get_pipeline)
|
||||
.and_return({ "number" => 1, "status" => "success" })
|
||||
allow(ci).to receive(:run).with(repo.remote_repo_id, "latest")
|
||||
.and_return(WarpEngine::CI::Run.new(number: 1, status: "success"))
|
||||
|
||||
get "/api/ci/pipelines/#{repo.id}/status"
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
json = JSON.parse(response.body)
|
||||
expect(json["pipeline"]["repo_name"]).to eq("game")
|
||||
expect(json["latest_run"]["status"]).to eq("success")
|
||||
end
|
||||
|
||||
it "answers without a run when the provider is unreachable" do
|
||||
repo = create(:pipeline)
|
||||
allow(ci).to receive(:run).and_raise(WarpEngine::CI::ConnectionError, "down")
|
||||
|
||||
get "/api/ci/pipelines/#{repo.id}/status"
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(JSON.parse(response.body)["latest_run"]).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
@@ -53,12 +64,10 @@ RSpec.describe "CI API", type: :request do
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it "triggers a pipeline with valid secret" do
|
||||
it "triggers a pipeline with a valid secret" do
|
||||
repo = create(:pipeline, repo_owner: "org", repo_name: "game")
|
||||
client = instance_double(WarpEngine::WoodpeckerClient)
|
||||
allow(WarpEngine::WoodpeckerClient).to receive(:new).and_return(client)
|
||||
allow(client).to receive(:trigger_pipeline)
|
||||
.and_return({ "number" => 7, "status" => "pending" })
|
||||
allow(ci).to receive(:trigger).with(repo.remote_repo_id, branch: "main")
|
||||
.and_return(WarpEngine::CI::Run.new(number: 7, status: "pending"))
|
||||
|
||||
post "/api/ci/pipelines/#{repo.id}/trigger",
|
||||
headers: { "X-Update-Secret" => "s3cret" }
|
||||
@@ -66,6 +75,7 @@ RSpec.describe "CI API", type: :request do
|
||||
expect(response).to have_http_status(:ok)
|
||||
json = JSON.parse(response.body)
|
||||
expect(json["triggered"]).to be true
|
||||
expect(json["pipeline"]["number"]).to eq(7)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -15,7 +15,7 @@ RSpec.describe "the WarpEngine-Version header", type: :request do
|
||||
end
|
||||
|
||||
it "is on an error response" do
|
||||
get "/api/image/999999"
|
||||
get "/api/softwares/no-such-software/builds"
|
||||
|
||||
expect(response).to have_http_status(:not_found)
|
||||
expect(response.headers["WarpEngine-Version"]).to eq(WarpEngine::VERSION)
|
||||
|
||||
Reference in New Issue
Block a user