Create the pika-forge-model crate and workspace registration
Intent: Establish a new shared library crate that will house all forge API model types, and register it in the workspace so both consumers can depend on it.
Affected files: crates/pika-forge-model/Cargo.toml, Cargo.toml, Cargo.lock
Evidence
@@ -0,0 +1,8 @@
+[package]
+name = "pika-forge-model"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+serde = { workspace = true, features = ["derive"] }
+serde_json = { workspace = true }
@@ -4,6 +4,7 @@ members = [
"rust",
"uniffi-bindgen",
"cli",
+ "crates/pika-forge-model",
@@ -32,6 +33,7 @@ mdk-storage-traits = ...
+pika-forge-model = { path = "crates/pika-forge-model" }
A new crate crates/pika-forge-model is created with minimal dependencies (serde and serde_json). It is added to the workspace members list in the root Cargo.toml and given a workspace-level path alias so downstream crates can reference it as pika-forge-model = { path = "../pika-forge-model" } or via workspace inheritance.
The crate intentionally has no runtime dependencies beyond serialization, keeping it lightweight for both server and CLI consumers.
Define the string_enum! macro for forward-compatible status enums
Intent: Provide a single macro that generates enums which serialize/deserialize as plain strings, support an Unknown(String) catch-all variant for forward compatibility, and implement Display, From<&str>, and From<String>.
Affected files: crates/pika-forge-model/src/lib.rs
Evidence
@@ -0,0 +1,394 @@
+use std::fmt;
+
+use serde::{Deserialize, Deserializer, Serialize, Serializer};
+
+macro_rules! string_enum {
+ (
+ $(#[$meta:meta])*
+ pub enum $name:ident {
+ $($variant:ident => $wire:literal),+ $(,)?
+ }
+ ) => {
+ $(#[$meta])*
+ #[derive(Debug, Clone, PartialEq, Eq, Hash)]
+ pub enum $name {
+ $($variant,)+
+ Unknown(String),
+ }
The string_enum! macro is the core abstraction of the new crate. For each invocation it generates:
- An enum with all named variants plus
Unknown(String) for unrecognized wire values.
as_str(&self) -> &str returning the canonical wire representation.
From<&str> and From<String> conversions that map known strings to variants and everything else to Unknown.
- Custom
Serialize / Deserialize implementations that go through the string representation.
- A
Display implementation delegating to as_str().
This means the server can add new status values without breaking older CLI clients — they simply deserialize into Unknown("new_value") and round-trip cleanly.
Define all shared enums using the macro
Intent: Enumerate every status domain (branch state, CI status, lane status, execution reason, failure kind, target health) as typed enums with known wire values, plus semantic helper methods.
Affected files: crates/pika-forge-model/src/lib.rs
Evidence
@@ +76,0 +76,48 @@
+string_enum! {
+ pub enum BranchState {
+ Open => "open",
+ Merged => "merged",
+ Closed => "closed",
+ }
+}
+
+string_enum! {
+ pub enum ForgeCiStatus {
+ Queued => "queued",
+ Running => "running",
+ Success => "success",
+ Failed => "failed",
+ ...
+ }
+}
+
+impl ForgeCiStatus {
+ pub fn is_active(&self) -> bool {
+ matches!(self, Self::Queued | Self::Running)
+ }
+
+ pub fn is_success(&self) -> bool {
+ matches!(self, Self::Success)
+ }
+}
Six enums are defined via the macro:
BranchState — Open, Merged, Closed
TutorialStatus — Pending, Ready, Failed
ForgeCiStatus — 20+ variants covering every known CI status, with is_active() and is_success() helpers
CiLaneStatus — lane-specific subset (Queued, Running, Success, Failed, Skipped, Lost, TimedOut, Cancelled) with its own is_active()
CiLaneExecutionReason — with label() returning human-readable text and a Default impl yielding Queued
CiLaneFailureKind — TestFailure, Timeout, Infrastructure with label()
CiTargetHealthState — Healthy, Unhealthy
Having distinct ForgeCiStatus vs CiLaneStatus types prevents accidental mixing of run-level and lane-level statuses.
Move shared response structs into pika-forge-model
Intent: Extract all API response/request structs that were duplicated between the server and CLI into the shared crate, making fields public and using the new typed enums instead of raw strings.
Affected files: crates/pika-forge-model/src/lib.rs
Evidence
@@ +220,0 +220,60 @@
+pub struct BranchSummary {
+ pub branch_id: i64,
+ pub repo: String,
+ ...
+ pub tutorial_status: TutorialStatus,
+ pub ci_status: ForgeCiStatus,
+ pub error_message: Option<String>,
+}
+
+pub struct CiRun {
+ pub id: i64,
+ ...
+ pub status: ForgeCiStatus,
+ #[serde(default)]
+ pub status_tone: Option<String>,
+ ...
+}
+
+pub struct CiLane {
+ ...
+ pub status: CiLaneStatus,
+ ...
+ pub execution_reason: CiLaneExecutionReason,
+ pub failure_kind: Option<CiLaneFailureKind>,
+ ...
+}
All response structs (BranchResolveResponse, BranchSummary, BranchDetailResponse, CiRun, CiLane, BranchLogsResponse, NightlyDetailResponse, LaneMutationResponse, RecoverRunResponse, WakeCiResponse, BranchActionResponse) are moved here with pub visibility and both Serialize + Deserialize.
Key changes compared to the old per-crate definitions:
- Status fields that were
String are now their respective enum types.
- New optional fields are added with
#[serde(default)]: status_tone, status_badge_class, is_failed, execution_reason_label, failure_kind_label, timing_summary, last_heartbeat_at, lease_expires_at, operator_hint.
BranchLogsResponse gains generic type parameters for pikaci-specific payload types, defaulting to serde_json::Value so the CLI can deserialize without knowing server-internal types.
Unit tests verify unknown status round-tripping and default deserialization of execution_reason.
Wire pika-forge-model into the ph CLI crate
Intent: Replace the ~190 lines of locally defined API structs in ph with re-exports from the shared crate, and update all call sites to use typed enum methods.
Affected files: crates/ph/Cargo.toml, crates/ph/src/api.rs, crates/ph/src/commands.rs, crates/ph/src/tests.rs
Evidence
@@ -12,6 +12,7 @@ anyhow = { workspace = true }
+pika-forge-model = { path = "../pika-forge-model" }
@@ -1,6 +1,13 @@
+#[allow(unused_imports)]
+pub(crate) use pika_forge_model::{
+ BranchActionResponse, BranchDetailResponse, ...
+};
@@ -30,194 +37,6 @@ pub(crate) struct MeResponse {
-#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
-pub(crate) struct BranchResolveResponse {
- ...
@@ -83,7 +83,7 @@
- return if branch.branch.ci_status == "success" {
+ return if branch.branch.ci_status.is_success() {
@@ -276,13 +276,9 @@
- matches!(branch.branch.ci_status.as_str(), "queued" | "running")
+ branch.branch.ci_status.is_active()
ph/Cargo.toml adds the pika-forge-model dependency. In api.rs, the large block of struct and enum definitions (lines 30–224 in the old file) is deleted and replaced with a single pub(crate) use pika_forge_model::{...} re-export.
In commands.rs, every raw string comparison is replaced with the typed method:
branch.branch.ci_status == "success" → branch.branch.ci_status.is_success()
matches!(status.as_str(), "queued" | "running") → status.is_active()
lane.failure_kind changes from Copy enum to Option<CiLaneFailureKind> reference (.as_ref() added)
CiTargetHealthState comparison now uses matches!(...as_ref(), Some(...))
CiLaneExecutionReason comparison uses .as_str() on both sides since the type is no longer Copy
In tests.rs, test fixtures are updated to construct enum variants directly (BranchState::Open, ForgeCiStatus::Running, CiLaneStatus::Queued) and populate the new optional fields (status_tone, timing_summary, operator_hint, etc.).
Wire pika-forge-model into the pika-news server crate
Intent: Replace ad-hoc response structs and serde_json::json!() calls in the server with the shared typed structs, adding mapping functions to convert internal DB records into the shared API types.
Affected files: crates/pika-news/Cargo.toml, crates/pika-news/src/web.rs
Evidence
@@ -22,6 +22,7 @@ hmac = { workspace = true }
+pika-forge-model = { path = "../pika-forge-model" }
@@ -383,48 +397,6 @@
-struct ForgeBranchResolveResponse {
- branch_id: i64,
- ...
-struct ForgeBranchLogsResponse {
- ...
-struct ForgeNightlyDetailResponse {
- ...
@@ -2133,18 +2089,112 @@
+fn map_api_ci_run(run: BranchCiRunRecord, now: DateTime<Utc>) -> CiRun {
+ let view = map_ci_run_view(run, now);
+ CiRun {
+ id: view.id,
+ ...
+ }
+}
+
+fn map_api_ci_lane_from_view(view: CiLaneView) -> CiLane {
+ CiLane {
+ ...
+ status: ForgeCiLaneStatus::from(view.status),
+ ...
+ }
+}
@@ -2210,11 +2260,12 @@
- }) => Json(serde_json::json!({
- "status": "ok",
- "branch_id": branch_id,
+ }) => Json(BranchActionResponse {
+ status: "ok".to_string(),
+ branch_id,
pika-news/Cargo.toml adds the shared dependency. In web.rs:
-
Deleted structs: ForgeBranchResolveResponse, ForgeBranchSummaryResponse, ForgeBranchDetailResponse, ForgeBranchLogsResponse, and ForgeNightlyDetailResponse are removed (~60 lines). They are replaced by imports from pika_forge_model with type aliases where names differ.
-
Type alias for logs: ForgeBranchLogsResponse is defined as SharedForgeBranchLogsResponse<CiLane, RunRecord, RunLogsMetadata, PreparedOutputsRecord>, specializing the generic parameters with server-specific pikaci types.
-
New mapping functions: Four functions are added to convert internal DB/view records into the shared API types:
map_api_ci_run — wraps existing map_ci_run_view and converts to CiRun
map_api_ci_lane — wraps map_ci_lane_view for branch lanes
map_api_ci_lane_from_view — direct CiLaneView → CiLane conversion
map_api_nightly_lane — wraps map_nightly_lane_view for nightly lanes
-
Handler updates: All forge API handlers (merge_handler, close_handler, fail_branch_ci_lane_handler, requeue_branch_ci_lane_handler, recover_branch_ci_run_handler, fail_nightly_lane_handler, requeue_nightly_lane_handler, recover_nightly_run_handler, wake_ci_handler, api_forge_branch_resolve_handler, api_forge_branch_detail_handler, api_forge_branch_logs_handler, api_forge_nightly_detail_handler) are updated from Json(serde_json::json!({...})) to Json(TypedStruct { ... }). String fields like branch_state and ci_status are now constructed via BranchState::from(...) and ForgeCiStatus::from(...) respectively.
This ensures the server's JSON responses exactly match the struct shapes the CLI deserializes, catching any schema drift at compile time.