stow_types/api.rs
1//! HTTP wire types exchanged between the CLI, the edge worker, the
2//! scheduler Durable Object, and trusted CI.
3//!
4//! Field docs describe the wire meaning of each payload; the validated
5//! identity newtypes from [`crate::identity`] carry the invariants.
6
7use std::collections::BTreeMap;
8
9use serde::{Deserialize, Serialize};
10use utoipa::ToSchema;
11
12use crate::artifact::{ArtifactKind, RustCrateType};
13use crate::identity::{
14 CMetadata, CrateName, CrateVersion, DependencyCMetadataJson, FeaturesJson, TargetTriple,
15 WireRustcVersion,
16};
17use crate::index::ArtifactIndexRow;
18use crate::platform::Profile;
19
20/// The compilation target triples the trusted CI build fleet covers.
21///
22/// The runner map in `build-crate.yml` builds for exactly this set, so
23/// `POST /api/v1/requests` expands every requested crate onto each of
24/// them. The set is `WaterUI`'s shipping matrix (water-rs/stow#90);
25/// the ESP32 `*-espidf` triples stay out because they need a forked
26/// toolchain. Discontinued platforms stay out too — Intel Macs
27/// (`x86_64-apple-darwin`, dropped by macOS 27), x86 Android, and
28/// 32-bit ARM Android.
29pub const CI_TARGET_TRIPLES: &[&str] = &[
30 "aarch64-apple-darwin",
31 "aarch64-apple-ios",
32 "aarch64-apple-ios-sim",
33 "aarch64-linux-android",
34 "x86_64-unknown-linux-gnu",
35 "aarch64-unknown-linux-gnu",
36 "x86_64-pc-windows-msvc",
37 "aarch64-pc-windows-msvc",
38 "wasm32-unknown-unknown",
39];
40
41/// Whether trusted CI has a runner that can build this target.
42///
43/// A target outside the set resolves to an empty `runs-on` in
44/// `build-crate.yml`, and the dispatched run then dies before any job
45/// starts — no job, no log, no completion report, and the queue slot spent
46/// until the stale-dispatch sweep reclaims it. Every path that can put a
47/// task in the queue checks this first.
48#[must_use]
49pub fn is_ci_target(target: &str) -> bool {
50 CI_TARGET_TRIPLES.contains(&target)
51}
52
53/// The GitHub Actions runner pool a CI target builds on.
54///
55/// Mirrors the `runs-on` map in `build-crate.yml`: `macos-14` for the
56/// Apple targets, `windows-latest` for the MSVC targets, `ubuntu-latest`
57/// for the rest. The scheduler caps dispatches per family because the
58/// pools are sized very differently — the org's macOS pool is the
59/// smallest — and starts the slow Windows legs of a wave first.
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
61pub enum RunnerFamily {
62 /// `ubuntu-latest` — also hosts the Android and wasm builds.
63 Linux,
64 /// `macos-14` — the smallest pool in the org's runner fleet.
65 MacOs,
66 /// `windows-latest` — the slowest legs of a full wave.
67 Windows,
68}
69
70impl RunnerFamily {
71 /// Every family, for iteration.
72 pub const ALL: [Self; 3] = [Self::Linux, Self::MacOs, Self::Windows];
73
74 /// The [`CI_TARGET_TRIPLES`] members that build on this family's
75 /// runner.
76 #[must_use]
77 pub const fn targets(self) -> &'static [&'static str] {
78 match self {
79 Self::Linux => &[
80 "aarch64-linux-android",
81 "x86_64-unknown-linux-gnu",
82 "aarch64-unknown-linux-gnu",
83 "wasm32-unknown-unknown",
84 ],
85 Self::MacOs => &[
86 "aarch64-apple-darwin",
87 "aarch64-apple-ios",
88 "aarch64-apple-ios-sim",
89 ],
90 Self::Windows => &["x86_64-pc-windows-msvc", "aarch64-pc-windows-msvc"],
91 }
92 }
93}
94
95/// Which runner family `build-crate.yml` dispatches this target to, or
96/// `None` for a target its `runs-on` map does not name.
97#[must_use]
98pub fn runner_family(target: &str) -> Option<RunnerFamily> {
99 RunnerFamily::ALL
100 .into_iter()
101 .find(|family| family.targets().contains(&target))
102}
103
104/// The task the scheduler dispatches to `stow-build`, carried verbatim as the
105/// `workflow_dispatch` input of the trusted build workflow.
106///
107/// Simple: just crate + target. CI figures out features/deps via `cargo metadata`.
108#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
109pub struct BuildTaskPayload {
110 /// Opaque scheduler task identifier (blake3 of identity tuple).
111 pub task_id: String,
112 /// Which queue attempt this dispatch carries. The scheduler bumps a
113 /// row's attempt every time a re-request resurrects it out of
114 /// failed/completed, and `complete` only applies a report whose attempt
115 /// matches the row's live one — a stale or duplicate report is a
116 /// conflict, never a silent overwrite of a newer attempt's state.
117 /// Defaults to 0 so a payload serialized before the field existed still
118 /// decodes; attempt 0 matches no row (attempts start at 1), so such a
119 /// report is rejected rather than applied blindly.
120 #[serde(default)]
121 pub attempt: u32,
122 /// Crate name as known to crates.io.
123 pub crate_name: CrateName,
124 /// Exact crate version to build.
125 pub version: CrateVersion,
126 /// Canonicalized features list (sorted, deduplicated).
127 pub features_json: FeaturesJson,
128 /// Compilation target triple.
129 pub target: TargetTriple,
130 /// Stable rustc version (e.g. `"1.83.0"`).
131 pub rustc_version: WireRustcVersion,
132 /// When true, the trusted build runner keeps the bundled `Cargo.lock` from
133 /// the crates.io tarball instead of removing it. Used by the top-binaries
134 /// preheat (`stow-admin preheat top-binaries`) so transitive `c_metadata`
135 /// matches what `cargo install --locked <bin>` would produce on the user's
136 /// machine. Defaults to false to preserve the historical "build against
137 /// latest semver-compatible deps" behavior for library preheats.
138 #[serde(default)]
139 pub preserve_lockfile: bool,
140}
141
142/// Artifact record CI POSTs to the edge's register endpoint after a build.
143///
144/// Sent to `/api/v1/admin/artifacts/register` once the build, sign, and OCI
145/// push have all succeeded. The edge worker authenticates the caller's
146/// GitHub identity (Actions OIDC for CI, push-user token otherwise) and
147/// persists the row in D1.
148#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
149pub struct ArtifactRecord {
150 /// Stable hash of the trusted build's exact rustc invocation identity.
151 pub compile_key: String,
152 /// Cargo's `-C metadata` value — part of the composite cache lookup key.
153 pub c_metadata: CMetadata,
154 /// Cargo's `-C extra-filename` suffix from the trusted build.
155 pub extra_filename: String,
156 /// Compilation target triple.
157 pub target: TargetTriple,
158 /// Rustc version string (e.g., "1.83.0").
159 pub rustc_version: WireRustcVersion,
160 /// Exact profile observed from the captured rustc invocation.
161 pub profile: Profile,
162 /// Exact `--emit` modes observed from the captured rustc invocation.
163 /// Must be strictly sorted and deduplicated.
164 pub emit: Vec<String>,
165 /// Crate name (for analytics/display).
166 pub crate_name: CrateName,
167 /// Crate version.
168 pub version: CrateVersion,
169 /// JSON-encoded feature set; canonicalized (sorted + deduplicated).
170 pub features_json: FeaturesJson,
171 /// JSON-encoded dependency `c_metadata` identities captured from rustc
172 /// --extern inputs; sorted by `(crate_name, c_metadata)`.
173 pub dependency_c_metadata_json: DependencyCMetadataJson,
174 /// OCI reference (e.g., "ghcr.io/water-rs/stow-cache:serde.1.0.0-...").
175 pub oci_reference: String,
176 /// OCI manifest digest (e.g., "sha256:...").
177 pub oci_digest: String,
178 /// Whether this artifact has native (C/C++) components.
179 pub has_native: bool,
180 /// Primary artifact kind for OCI naming and analytics.
181 pub artifact_kind: ArtifactKind,
182 /// Declared Rust crate types from cargo metadata / rustc args.
183 pub crate_types: Vec<RustCrateType>,
184 /// Artifact size in bytes: the sum of the uncompressed output files.
185 pub artifact_size: u64,
186 /// Digest (`sha256:…`) of the assembled bundle tar the trusted publish
187 /// stage pushed as the `<tag>.bundle` layer. The edge streams exactly
188 /// this blob to CLIs; it is what the byte path is keyed and fetched by.
189 pub bundle_digest: String,
190 /// Size in bytes of the bundle tar — the exact `content-length` of a
191 /// bundle GET and the input to the Cache API size gate.
192 pub bundle_size: u64,
193 /// Wall-clock milliseconds the captured rustc invocation took — what a
194 /// served hit on this artifact is credited as CPU time saved.
195 pub compile_millis: u64,
196}
197
198/// Request body for `POST /api/v1/admin/artifacts/register`.
199///
200/// `task_id` binds the record set to the scheduler task the calling run
201/// was dispatched for: the edge requires the task to be in flight and
202/// every record to belong to the task's dependency closure before it
203/// writes a row. The Actions OIDC identity must name a task; a repo-push
204/// caller (the operator/backfill path) may omit it.
205#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
206pub struct RegisterArtifactsRequest {
207 /// Scheduler task id the registering run was dispatched for
208 /// (`BuildTaskPayload::task_id`). Required from the Actions OIDC
209 /// identity; optional for repo-push callers.
210 #[serde(default)]
211 pub task_id: Option<String>,
212 /// Artifact records to upsert into the catalog.
213 pub records: Vec<ArtifactRecord>,
214}
215
216/// Request body for scheduler task submission.
217#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
218pub struct EnqueueRequest {
219 /// Crate name to build.
220 pub crate_name: CrateName,
221 /// Crate version to build.
222 pub version: CrateVersion,
223 /// Canonical features list.
224 pub features_json: FeaturesJson,
225 /// Compilation target triple.
226 pub target: TargetTriple,
227 /// Stable rustc version.
228 pub rustc_version: WireRustcVersion,
229 /// Total download count from crates.io (used for priority calculation).
230 pub downloads: u64,
231 /// Source of the enqueue request.
232 pub source: EnqueueSource,
233 /// Task-level dependencies that must be completed before this task can dispatch.
234 #[serde(default)]
235 pub depends_on: Vec<EnqueueDependency>,
236 /// Mirrors `BuildTaskPayload::preserve_lockfile`. Set to true for binary-
237 /// derived overlay enqueues so the trusted build resolves transitive deps
238 /// against the binary's published `Cargo.lock`.
239 #[serde(default)]
240 pub preserve_lockfile: bool,
241}
242
243/// One task-level dependency that must complete before its parent dispatches.
244#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
245pub struct EnqueueDependency {
246 /// Dependency crate name.
247 pub crate_name: CrateName,
248 /// Dependency crate version.
249 pub version: CrateVersion,
250 /// Canonical features list.
251 pub features_json: FeaturesJson,
252 /// Compilation target triple.
253 pub target: TargetTriple,
254 /// Stable rustc version.
255 pub rustc_version: WireRustcVersion,
256}
257
258/// Where an enqueue request originated.
259#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
260pub enum EnqueueSource {
261 /// Watcher detected a crate version update.
262 CrateUpdate,
263 /// Watcher detected a new rustc stable release.
264 RustcUpdate,
265 /// Edge reported a cache miss.
266 CacheMiss,
267 /// A Turnstile-verified human submitted `POST /api/v1/requests`. Tasks
268 /// enqueued with this source land in the scheduler's human lane.
269 HumanRequest,
270}
271
272/// Admission ticket the edge mints for one canonical enqueue task when a
273/// public request misses the cache.
274///
275/// Returned inside miss responses — as the 404 body of
276/// `POST /api/v1/artifacts/semantic` and in
277/// `DependencyGraphResponse::miss_admissions` — so the fetch path stays
278/// cheap. The client redeems the ticket by solving its proof-of-work and
279/// posting an [`EnqueueTicket`] to `POST /api/v1/enqueue`.
280#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
281pub struct EnqueueAdmission {
282 /// Canonical scheduler task id (blake3-derived identity string).
283 pub task_id: String,
284 /// Server-issued challenge — the hex HMAC-SHA256 over
285 /// `task_id ‖ canonical request JSON ‖ issue_minute` under the edge's
286 /// `STOW_POW_CHALLENGE_SECRET`. Opaque to clients; accepted during its
287 /// issue minute and the minute after it.
288 pub challenge: String,
289 /// Leading zero bits the client's
290 /// `blake3(task_id ‖ challenge ‖ nonce)` digest must show for
291 /// `/enqueue` to accept the ticket. `0` means the queue is shallow
292 /// enough that admission is free.
293 pub difficulty: u32,
294 /// The canonical enqueue request this admission authorizes. The edge is
295 /// stateless: the client echoes `request` back in its ticket and
296 /// `/api/v1/enqueue` forwards it to the scheduler after verifying the
297 /// challenge binds it.
298 pub request: EnqueueRequest,
299}
300
301/// Request body for `POST /api/v1/enqueue`: the redemption of an
302/// [`EnqueueAdmission`].
303#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
304pub struct EnqueueTicket {
305 /// Canonical task id carried by the admission.
306 pub task_id: String,
307 /// The admission's server-issued challenge.
308 pub challenge: String,
309 /// Client-computed nonce such that
310 /// `blake3(task_id ‖ challenge ‖ nonce)` has at least the required
311 /// number of leading zero bits.
312 pub nonce: u64,
313 /// The canonical enqueue request from the admission. The edge
314 /// recomputes the challenge HMAC over this payload and forwards it to
315 /// the scheduler — no server-side request lookup.
316 pub request: EnqueueRequest,
317}
318
319/// Request body for `POST /api/v1/admissions`: the misses a client's
320/// local index resolution found, plus the resolved graph the edge needs
321/// to re-derive the enqueue set with dominator pruning.
322///
323/// The client's dependency graph never leaves the machine in raw form for
324/// *coverage* — this call happens only when the local resolver already
325/// decided entries are uncovered, and the edge re-checks coverage against
326/// the catalog before minting anything.
327#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
328pub struct AdmissionRequest {
329 /// Compilation target triple.
330 pub target: TargetTriple,
331 /// Stable rustc version.
332 pub rustc_version: WireRustcVersion,
333 /// Direct-dep entries the local resolver found uncovered.
334 pub entries: Vec<DependencyGraphEntry>,
335 /// The client's resolved transitive graph.
336 #[serde(default)]
337 pub expanded_entries: Vec<ResolvedDependencyGraphEntry>,
338}
339
340/// CI reports job completion to the scheduler DO.
341#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
342pub struct BuildCompleteReport {
343 /// Scheduler task identifier, echoing `BuildTaskPayload::task_id`.
344 pub task_id: String,
345 /// Queue attempt this report belongs to, echoing
346 /// `BuildTaskPayload::attempt`. The scheduler applies the report only
347 /// when it matches the row's live attempt in a dispatched/running
348 /// state; anything else is a stale or duplicate report and conflicts.
349 /// Defaults to 0, which matches no row (attempts start at 1).
350 #[serde(default)]
351 pub attempt: u32,
352 /// Whether the build, sign, push, and registration all succeeded.
353 pub success: bool,
354 /// The build stopped early: a cargo phase exited non-zero before the
355 /// unit graph finished, and the run registered the artifacts it
356 /// captured up to the failure instead of nothing at all. Only
357 /// meaningful when `success` is false — `artifacts_uploaded` then
358 /// counts the published prefix, never the task's whole closure.
359 /// Absent from reports written before the field existed.
360 #[serde(default)]
361 pub partial: bool,
362 /// Failure description when `success` is false.
363 pub error: Option<String>,
364 /// Number of artifacts uploaded (including transitive deps).
365 pub artifacts_uploaded: u32,
366 /// GitHub Actions run id the report came from. CI leaves it `None`;
367 /// the edge overwrites it with the OIDC token's `run_id` claim before
368 /// forwarding, so the queue row carries the run that produced it.
369 #[serde(default)]
370 pub github_run_id: Option<String>,
371}
372
373/// A normalized dependency entry from a resolved Cargo dependency graph.
374#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, ToSchema)]
375pub struct DependencyGraphEntry {
376 /// Crate name.
377 pub crate_name: CrateName,
378 /// Crate version.
379 #[schema(value_type = String)]
380 pub version: semver::Version,
381 /// Sorted, deduplicated features (raw list — wire form is JSON array).
382 pub features: Vec<String>,
383}
384
385/// One exact dependency edge in a client-resolved Cargo graph.
386#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, ToSchema)]
387pub struct ResolvedDependencyGraphDependency {
388 /// Dependency crate name.
389 pub crate_name: CrateName,
390 /// Dependency crate version.
391 #[schema(value_type = String)]
392 pub version: semver::Version,
393}
394
395/// One exact crates.io package node resolved from the client's current lockfile graph.
396#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
397pub struct ResolvedDependencyGraphEntry {
398 /// Package crate name.
399 pub crate_name: CrateName,
400 /// Package crate version.
401 #[schema(value_type = String)]
402 pub version: semver::Version,
403 /// Sorted, deduplicated features (raw list — wire form is JSON array).
404 pub features: Vec<String>,
405 /// Direct dependencies of this package.
406 pub dependencies: Vec<ResolvedDependencyGraphDependency>,
407}
408
409/// The anonymous-traffic circuit breaker ("panic switch").
410///
411/// Held by the scheduler Durable Object. `enabled: true` makes every
412/// anonymous edge route answer `503 Service Unavailable` while the trusted
413/// `/api/v1/admin/*` and `/api/v1/scheduler/*` routes keep working. Wire
414/// shape of `GET`/`POST /api/v1/admin/panic` and of the scheduler object's
415/// `/panic` routes.
416#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
417pub struct PanicSwitch {
418 /// Whether anonymous traffic is being shed.
419 pub enabled: bool,
420}
421
422/// Scheduler DO queue status for monitoring.
423#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
424pub struct SchedulerStatus {
425 /// Tasks waiting to become dispatchable.
426 pub pending: u32,
427 /// Pending tasks in the human lane — a subset of `pending` counted
428 /// separately so operators can see human-requested work.
429 pub human_pending: u32,
430 /// Tasks whose `workflow_dispatch` was sent but not yet picked up.
431 pub dispatched: u32,
432 /// Tasks a CI run has claimed but not yet reported complete.
433 pub running: u32,
434 /// Tasks that completed successfully.
435 pub completed: u32,
436 /// Tasks whose CI run reported failure.
437 pub failed: u32,
438 /// Tasks whose CI run stopped early but registered the artifacts it
439 /// produced. Absent from responses written before the field existed.
440 #[serde(default)]
441 pub partial: u32,
442}
443
444/// Request body for `POST /api/v1/requests`: a human asking for one crate
445/// to be built into the public cache.
446///
447/// The endpoint is the submission path behind the request form on
448/// `stow.waterui.dev`; the Turnstile token is the admission check and every
449/// accepted request lands in the scheduler's human lane ahead of the
450/// cache-miss queue.
451#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
452pub struct CrateRequest {
453 /// Crate name as published on crates.io.
454 pub crate_name: CrateName,
455 /// Exact version to request. When `None` the edge resolves the newest
456 /// non-prerelease, non-yanked crates.io version.
457 #[serde(default)]
458 pub version: Option<CrateVersion>,
459 /// Features to enable for the requested crate, in the canonical
460 /// [`FeaturesJson`] representation. The list is taken literally:
461 /// `["default", ...]` builds with default features on, and an empty
462 /// list is `--no-default-features` with nothing added.
463 pub features_json: FeaturesJson,
464 /// Cloudflare Turnstile token produced by the invisible widget. Verified
465 /// against siteverify before the edge does any resolution work.
466 pub turnstile_token: String,
467}
468
469/// One crate in a `GET /api/v1/crates/search` result.
470#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
471pub struct CrateSearchHit {
472 /// Crate name as published on crates.io.
473 pub crate_name: CrateName,
474 /// The crate's one-line description, when it has one.
475 #[serde(default)]
476 pub description: Option<String>,
477 /// Newest version crates.io lists for the crate.
478 pub max_version: CrateVersion,
479 /// All-time download count, the ordering crates.io search returns.
480 pub downloads: u64,
481}
482
483/// Response body for `GET /api/v1/crates/search`.
484#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
485pub struct CrateSearchResponse {
486 /// Matching crates, most downloaded first, capped by the `limit` query
487 /// parameter.
488 pub crates: Vec<CrateSearchHit>,
489}
490
491/// Response body for `GET /api/v1/crates/{crate_name}/versions`.
492#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
493pub struct CrateVersionsResponse {
494 /// Published, non-yanked versions, newest first.
495 pub versions: Vec<CrateVersion>,
496}
497
498/// One feature a crate version declares.
499#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
500pub struct CrateFeature {
501 /// The feature name, as written in the crate's `[features]` table or
502 /// implied by an optional dependency.
503 pub name: String,
504 /// The features and optional dependencies this one turns on. Empty for
505 /// an implicit optional-dependency feature.
506 pub implies: Vec<String>,
507 /// Whether the crate's `default` feature set enables this feature,
508 /// directly or transitively.
509 pub default: bool,
510}
511
512/// Response body for `GET /api/v1/crates/{crate_name}/versions/{version}/features`.
513#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
514pub struct CrateFeaturesResponse {
515 /// Every selectable feature of the version, `default` first and the
516 /// rest alphabetical.
517 pub features: Vec<CrateFeature>,
518}
519
520/// Which scheduler lane a queue row belongs to.
521#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
522#[serde(rename_all = "snake_case")]
523pub enum TaskLane {
524 /// Filled by the cache-miss path and watchers; subject to
525 /// `STOW_DISPATCH_MIN_AGE_MINUTES` before dispatch.
526 Miss,
527 /// Filled by the Turnstile-verified human request API. Dispatches ahead
528 /// of the miss lane and bypasses the minimum-age hold.
529 Human,
530}
531
532impl TaskLane {
533 /// The stable string persisted in the scheduler's `lane` column.
534 #[must_use]
535 pub const fn as_str(self) -> &'static str {
536 match self {
537 Self::Miss => "miss",
538 Self::Human => "human",
539 }
540 }
541
542 /// Inverse of [`Self::as_str`] for values read back from queue rows.
543 #[must_use]
544 pub fn parse(value: &str) -> Option<Self> {
545 match value {
546 "miss" => Some(Self::Miss),
547 "human" => Some(Self::Human),
548 _ => None,
549 }
550 }
551}
552
553/// Lifecycle of one scheduler queue row.
554#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
555#[serde(rename_all = "snake_case")]
556pub enum QueueTaskStatus {
557 /// Waiting for dependencies or dispatch eligibility.
558 Pending,
559 /// `workflow_dispatch` sent, awaiting the CI job to claim it.
560 Dispatched,
561 /// A CI run claimed the task but has not reported completion.
562 Running,
563 /// Build, signing, push, and registration all succeeded.
564 Completed,
565 /// The CI run reported failure.
566 Failed,
567 /// The CI run stopped early but registered the artifacts it produced
568 /// before cargo failed — the task's own output is still missing, so
569 /// it is not a completion, but it did not land empty-handed either.
570 Partial,
571}
572
573impl QueueTaskStatus {
574 /// The stable string persisted in the scheduler's `status` column.
575 #[must_use]
576 pub const fn as_str(self) -> &'static str {
577 match self {
578 Self::Pending => "pending",
579 Self::Dispatched => "dispatched",
580 Self::Running => "running",
581 Self::Completed => "completed",
582 Self::Failed => "failed",
583 Self::Partial => "partial",
584 }
585 }
586
587 /// Inverse of [`Self::as_str`] for values read back from queue rows.
588 #[must_use]
589 pub fn parse(value: &str) -> Option<Self> {
590 match value {
591 "pending" => Some(Self::Pending),
592 "dispatched" => Some(Self::Dispatched),
593 "running" => Some(Self::Running),
594 "completed" => Some(Self::Completed),
595 "failed" => Some(Self::Failed),
596 "partial" => Some(Self::Partial),
597 _ => None,
598 }
599 }
600}
601
602/// Per-target state reported inside [`CrateRequestTarget`].
603#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
604#[serde(rename_all = "snake_case")]
605pub enum CrateRequestState {
606 /// The artifact already exists in the public cache for this target.
607 Cached,
608 /// This request enqueued the task into the human lane.
609 Queued,
610 /// The task was already in the queue (re-requesting promoted or
611 /// refreshed it) when this request arrived.
612 AlreadyQueued,
613 /// The task has been dispatched or is actively building.
614 Building,
615 /// The requested crate publishes no library target — a name source,
616 /// never a task. Its dependency closure is what the request enqueued.
617 ClosureQueued,
618}
619
620/// Per-target outcome inside a [`CrateRequestOutcome`].
621#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
622pub struct CrateRequestTarget {
623 /// Compilation target triple — one of [`CI_TARGET_TRIPLES`].
624 pub target: TargetTriple,
625 /// Where this target's task stands after the request.
626 pub state: CrateRequestState,
627 /// Scheduler task id for the requested crate on this target. Absent
628 /// when `state` is [`CrateRequestState::Cached`].
629 #[serde(default)]
630 pub task_id: Option<String>,
631 /// 1-based position among pending human-lane tasks in dispatch order;
632 /// `None` unless the task is still pending in the human lane.
633 #[serde(default)]
634 pub human_lane_position: Option<u32>,
635}
636
637/// Response of `POST /api/v1/requests`: what the edge resolved and where
638/// each supported target stands.
639#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
640pub struct CrateRequestOutcome {
641 /// Echoed crate name.
642 pub crate_name: CrateName,
643 /// The resolved version — the requested one, or the newest
644 /// non-prerelease, non-yanked crates.io release.
645 pub version: CrateVersion,
646 /// Current stable rustc version the enqueued tasks target.
647 pub rustc_version: WireRustcVersion,
648 /// Per-target outcomes in [`CI_TARGET_TRIPLES`] order.
649 pub targets: Vec<CrateRequestTarget>,
650}
651
652/// Point-in-time view of one scheduler task, returned by
653/// `GET /api/v1/requests/{task_id}`.
654#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
655pub struct RequestStatus {
656 /// Canonical scheduler task id (blake3-derived identity string).
657 pub task_id: String,
658 /// Crate name.
659 pub crate_name: CrateName,
660 /// Exact crate version.
661 pub version: CrateVersion,
662 /// Canonical features list.
663 pub features_json: FeaturesJson,
664 /// Compilation target triple.
665 pub target: TargetTriple,
666 /// Stable rustc version.
667 pub rustc_version: WireRustcVersion,
668 /// Which scheduler lane the task is queued in.
669 pub lane: TaskLane,
670 /// Current task lifecycle state.
671 pub status: QueueTaskStatus,
672 /// 1-based position among pending human-lane tasks in dispatch order;
673 /// `None` unless the task is a pending human-lane task.
674 #[serde(default)]
675 pub human_lane_position: Option<u32>,
676 /// Whether the task builds against the bundled `Cargo.lock`
677 /// (`EnqueueRequest::preserve_lockfile`), which decides whether the
678 /// task's dependency closure is reproducible from crates.io metadata.
679 #[serde(default)]
680 pub preserve_lockfile: bool,
681}
682
683/// Response body for `GET /api/v1/admin/index/{target}/{rustc_version}`.
684///
685/// One keyset page of the slice's servable artifact rows — the data
686/// `stow-admin index export` assembles into the published
687/// [`crate::index::ArtifactIndex`].
688#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
689pub struct ArtifactIndexPage {
690 /// Rows ordered by `c_metadata`, every one strictly after the
691 /// request's `after` cursor and carrying a published bundle.
692 pub rows: Vec<ArtifactIndexRow>,
693 /// The `after` cursor for the next page — the last row's `c_metadata`
694 /// when this page was full, `None` once the slice is exhausted.
695 pub next_after: Option<String>,
696}
697
698// ===== Operations API (`stow-admin` under `/api/v1/admin/*`) =====
699
700/// Selector for `GET /api/v1/admin/queue` and
701/// `POST /api/v1/admin/queue/{retry,cancel,promote,purge}`.
702///
703/// One flat struct on purpose: the same shape has to decode from a JSON
704/// body and from a query string, and a nested or `#[serde(flatten)]`ed
705/// half forces serde to buffer the query's values as strings, which no
706/// numeric field can then deserialize from. So `{"task_ids": […],
707/// "status": "failed"}` and `?task_ids=a&task_ids=b&status=failed&limit=5`
708/// decode identically, and the mutation preview lists exactly the rows a
709/// selector names.
710///
711/// Every predicate is optional; a selector with none of them selects
712/// every row (and is rejected for mutations — an operator mutation must
713/// name either explicit task ids or at least one predicate).
714///
715/// A non-empty `task_ids` selects exactly those rows and the predicates
716/// are ignored; otherwise the predicates select. Either way the verb's
717/// own status/lane predicates still apply — a mutation never touches a
718/// row outside its transition domain.
719#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
720pub struct QueueSelector {
721 /// Explicit task ids.
722 #[serde(default)]
723 pub task_ids: Vec<String>,
724 /// Lifecycle status to match.
725 #[serde(default)]
726 pub status: Option<QueueTaskStatus>,
727 /// Compilation target to match.
728 #[serde(default)]
729 pub target: Option<TargetTriple>,
730 /// Crate name to match (`crate` on the wire).
731 #[serde(default, rename = "crate", alias = "crate_name")]
732 pub crate_name: Option<CrateName>,
733 /// Only rows whose `updated_at` is at least this many seconds old
734 /// (`older_than` on the wire).
735 #[serde(default, rename = "older_than", alias = "older_than_secs")]
736 pub older_than_secs: Option<u64>,
737 /// Most rows a listing returns (bounded server-side); mutations
738 /// ignore it — a mutation selector either matches everything its
739 /// predicates describe or is rejected.
740 #[serde(default)]
741 pub limit: Option<u32>,
742}
743
744/// Response of the `queue retry|cancel|promote|purge` endpoints: how many
745/// rows the transition touched.
746#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
747pub struct QueueMutationResult {
748 /// Rows the transition affected.
749 pub affected: u32,
750}
751
752/// One queue row as `GET /api/v1/admin/queue` reports it.
753#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
754pub struct QueueTask {
755 /// Scheduler task identifier.
756 pub task_id: String,
757 /// Crate name to build.
758 pub crate_name: CrateName,
759 /// Crate version to build.
760 pub version: CrateVersion,
761 /// Canonical features list.
762 pub features_json: FeaturesJson,
763 /// Compilation target.
764 pub target: TargetTriple,
765 /// Rustc version the row builds for.
766 pub rustc_version: WireRustcVersion,
767 /// Dispatch lane the row is queued in.
768 pub lane: TaskLane,
769 /// Lifecycle status.
770 pub status: QueueTaskStatus,
771 /// Live enqueue epoch — completion reports only apply to this attempt.
772 pub attempt: u32,
773 /// Last dispatch/build error, empty when none.
774 pub error: String,
775 /// crates.io download count captured at enqueue time.
776 pub downloads: u64,
777 /// Cache misses this row is responsible for.
778 pub miss_count: u32,
779 /// Cache-hit requests this row has served demand for.
780 pub request_count: u32,
781 /// Dispatches attempted against this row.
782 pub dispatch_attempts: u32,
783 /// Whether the row builds against its checked-in lockfile.
784 pub preserve_lockfile: bool,
785 /// GitHub Actions run id the dispatched build reported back through
786 /// its OIDC-claimed register/complete calls.
787 #[serde(default)]
788 pub github_run_id: Option<String>,
789 /// First request timestamp (`YYYY-MM-DD HH:MM:SS` UTC).
790 pub first_requested_at: String,
791 /// Row creation timestamp.
792 pub created_at: String,
793 /// Last state-transition timestamp.
794 pub updated_at: String,
795}
796
797/// One in-flight (dispatched/running) queue row in [`AdminStatus`].
798#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
799pub struct AdminInFlight {
800 /// Scheduler task identifier.
801 pub task_id: String,
802 /// Crate name to build.
803 pub crate_name: CrateName,
804 /// Crate version to build.
805 pub version: CrateVersion,
806 /// Compilation target.
807 pub target: TargetTriple,
808 /// Rustc version the row builds for.
809 pub rustc_version: WireRustcVersion,
810 /// Lifecycle status (`dispatched` or `running`).
811 pub status: QueueTaskStatus,
812 /// Live enqueue epoch.
813 pub attempt: u32,
814 /// Dispatches attempted against this row.
815 pub dispatch_attempts: u32,
816 /// Last state-transition timestamp — the stale-dispatch lease clock.
817 pub updated_at: String,
818 /// GitHub Actions run id, once the run has reported back.
819 #[serde(default)]
820 pub github_run_id: Option<String>,
821}
822
823/// Per-target completion tallies over the trailing 24 hours.
824#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
825pub struct AdminTargetStats {
826 /// Compilation target.
827 pub target: TargetTriple,
828 /// Rows that completed in the window.
829 pub completed_24h: u32,
830 /// Rows that failed in the window.
831 pub failed_24h: u32,
832 /// Rows that stopped early in the window but registered what they
833 /// produced. Absent from responses written before the field existed.
834 #[serde(default)]
835 pub partial_24h: u32,
836}
837
838/// Response of `GET /api/v1/admin/status` — the scheduler's operator view.
839#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
840pub struct AdminStatus {
841 /// Pending rows in the cache-miss lane.
842 pub pending_miss: u32,
843 /// Pending rows in the human lane.
844 pub pending_human: u32,
845 /// Age in seconds of the oldest pending row (`first_requested_at`).
846 #[serde(default)]
847 pub oldest_pending_seconds: Option<u64>,
848 /// Dispatched/running rows, oldest transition first.
849 pub in_flight: Vec<AdminInFlight>,
850 /// Per-target completion tallies over the trailing 24 hours.
851 pub targets: Vec<AdminTargetStats>,
852 /// Whether the anonymous-traffic circuit breaker is engaged.
853 pub panic_enabled: bool,
854}
855
856/// Response of `POST /api/v1/scheduler/tasks/submit` — what a request batch
857/// became after canonicalization and enqueue.
858#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
859pub struct SchedulerSubmitResponse {
860 /// Requests that canonicalized and were handed to the scheduler.
861 pub submitted: u32,
862 /// Brand-new queue rows inserted; the rest of `submitted` updated or
863 /// merged into existing rows.
864 pub inserted: u32,
865 /// Requests dropped during canonicalization (unpublished version or
866 /// unresolvable `depends_on`).
867 pub dropped: u32,
868}
869
870/// Response of `GET /api/v1/admin/coverage/{crate}` — per-CI-target
871/// servable identities for one crate (one version when `version` was given).
872#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
873pub struct CrateCoverage {
874 /// Crate the coverage describes.
875 pub crate_name: CrateName,
876 /// Version the coverage is scoped to (`None` = every published version).
877 #[serde(default)]
878 pub version: Option<CrateVersion>,
879 /// One entry per [`CI_TARGET_TRIPLES`] target — or just the requested
880 /// target — in canonical order; an empty `artifacts` list means the
881 /// target has nothing servable.
882 pub targets: Vec<CoverageTarget>,
883}
884
885/// One target's servable identities inside [`CrateCoverage`].
886#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
887pub struct CoverageTarget {
888 /// Compilation target.
889 pub target: TargetTriple,
890 /// Servable identities (published bundle rows) for this target.
891 pub artifacts: Vec<CoverageArtifact>,
892}
893
894/// One servable artifact identity inside [`CoverageTarget`].
895#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
896pub struct CoverageArtifact {
897 /// Crate version the artifact serves.
898 pub version: CrateVersion,
899 /// Canonical features list the artifact was built with.
900 pub features_json: FeaturesJson,
901 /// Rustc version the artifact was built by.
902 pub rustc_version: WireRustcVersion,
903 /// Cargo `-C metadata` identity.
904 pub c_metadata: CMetadata,
905 /// Bundle tar size in bytes.
906 pub bundle_size: u64,
907}
908
909/// Request body for `POST /api/v1/admin/preheat/plan` — a dry run of the
910/// resolver's closure expansion and dominance pruning for one crate
911/// request. Nothing is enqueued.
912#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
913pub struct PreheatPlanRequest {
914 /// Crate name to plan for.
915 pub crate_name: CrateName,
916 /// Exact version; absent resolves the newest non-prerelease,
917 /// non-yanked published release.
918 #[serde(default)]
919 pub version: Option<CrateVersion>,
920 /// Seed features (`[]` = `--no-default-features` semantics).
921 pub features_json: FeaturesJson,
922 /// One compilation target, or absent for every [`CI_TARGET_TRIPLES`]
923 /// target.
924 #[serde(default)]
925 pub target: Option<TargetTriple>,
926 /// Rustc version; absent resolves the scheduler's stable channel
927 /// version.
928 #[serde(default)]
929 pub rustc_version: Option<WireRustcVersion>,
930}
931
932/// Response of `POST /api/v1/admin/preheat/plan`.
933#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
934pub struct PreheatPlanResponse {
935 /// Crate the plan was computed for.
936 pub crate_name: CrateName,
937 /// Version the request resolved to.
938 pub version: CrateVersion,
939 /// Rustc version the plan was computed for.
940 pub rustc_version: WireRustcVersion,
941 /// One plan per resolved target, in [`CI_TARGET_TRIPLES`] order.
942 pub targets: Vec<PreheatPlanTarget>,
943}
944
945/// One target's enqueue plan inside [`PreheatPlanResponse`].
946#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
947pub struct PreheatPlanTarget {
948 /// Compilation target.
949 pub target: TargetTriple,
950 /// Whether the requested crate already has a servable artifact here.
951 pub root_cached: bool,
952 /// Tasks the dispatch wave would enqueue — dominance-pruned, so
953 /// `depends_on` edges hold dominated rows until their dominator
954 /// resolves. Tasks with an empty `depends_on` are the wave's roots.
955 pub tasks: Vec<EnqueueRequest>,
956}
957
958/// Response of `GET /api/v1/admin/artifacts/{target}/{rustc_version}/{c_metadata}`:
959/// the D1 catalog row plus the bundle image's OCI manifest read from GHCR.
960#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
961pub struct ArtifactInspection {
962 /// The D1 catalog row.
963 pub record: ArtifactRecord,
964 /// The bundle image's OCI manifest (`manifests/<tag>.bundle`).
965 pub manifest: OciManifest,
966}
967
968/// OCI image manifest — the document a `manifests/<reference>` GET serves.
969#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
970pub struct OciManifest {
971 /// Manifest schema version (`2` for every served document).
972 pub schema_version: u32,
973 /// Manifest media type.
974 #[serde(default)]
975 pub media_type: Option<String>,
976 /// Config blob descriptor.
977 pub config: OciDescriptor,
978 /// Layer blob descriptors.
979 #[serde(default)]
980 pub layers: Vec<OciDescriptor>,
981 /// Manifest annotations.
982 #[serde(default)]
983 pub annotations: BTreeMap<String, String>,
984}
985
986/// One blob descriptor inside an [`OciManifest`].
987#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
988pub struct OciDescriptor {
989 /// Blob media type.
990 pub media_type: String,
991 /// `sha256:…` content digest.
992 pub digest: String,
993 /// Blob size in bytes.
994 pub size: u64,
995 /// Descriptor annotations.
996 #[serde(default)]
997 pub annotations: BTreeMap<String, String>,
998}
999
1000/// Query for `GET /api/v1/admin/artifacts` — a bounded catalog listing
1001/// used to preview a prune and for ad-hoc inspection.
1002#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
1003pub struct ArtifactListQuery {
1004 /// Only rows built by this rustc version.
1005 #[serde(default)]
1006 pub rustc_version: Option<WireRustcVersion>,
1007 /// Only rows for this compilation target.
1008 #[serde(default)]
1009 pub target: Option<TargetTriple>,
1010 /// Only rows for this crate (`crate` on the wire).
1011 #[serde(default, rename = "crate", alias = "crate_name")]
1012 pub crate_name: Option<CrateName>,
1013 /// Most rows to return (bounded server-side).
1014 #[serde(default)]
1015 pub limit: Option<u32>,
1016}
1017
1018/// Request body for `POST /api/v1/admin/artifacts/prune`.
1019///
1020/// Deletes every catalog row built by `rustc_version` — the retired
1021/// toolchain — and invalidates its lookup-cache entries. GHCR image tags
1022/// are not deleted; they age out under the package's own retention.
1023#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
1024pub struct ArtifactPruneRequest {
1025 /// Retired toolchain whose rows are pruned.
1026 pub rustc_version: WireRustcVersion,
1027}
1028
1029/// Response of `POST /api/v1/admin/artifacts/prune`.
1030#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
1031pub struct ArtifactPruneResponse {
1032 /// Catalog rows deleted.
1033 pub deleted: u32,
1034}
1035
1036/// Body for the scheduler DO's `/tasks/observe-run`.
1037///
1038/// Stamps the GitHub Actions run id onto an in-flight queue row so
1039/// `status` can surface a run URL. Stamping does not touch `updated_at`
1040/// — the stale-dispatch lease clock only moves on real state
1041/// transitions.
1042#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
1043pub struct ObserveRun {
1044 /// Task id the run was dispatched for.
1045 pub task_id: String,
1046 /// GitHub Actions run id from the OIDC token's `run_id` claim.
1047 pub github_run_id: String,
1048}
1049
1050#[cfg(test)]
1051mod tests {
1052 use super::{CI_TARGET_TRIPLES, RunnerFamily, runner_family};
1053
1054 /// Every CI target must land in exactly one family, and the families'
1055 /// `targets()` lists together must be exactly `CI_TARGET_TRIPLES` —
1056 /// drift between this mapping and the `runs-on` map in
1057 /// `build-crate.yml` would let the scheduler cap and order the wrong
1058 /// rows.
1059 #[test]
1060 fn runner_families_partition_ci_target_triples() {
1061 let mut mapped: Vec<&str> = Vec::new();
1062 for family in RunnerFamily::ALL {
1063 mapped.extend_from_slice(family.targets());
1064 }
1065 mapped.sort_unstable();
1066 let mut all = CI_TARGET_TRIPLES.to_vec();
1067 all.sort_unstable();
1068 assert_eq!(mapped, all);
1069 for target in CI_TARGET_TRIPLES {
1070 assert!(
1071 runner_family(target).is_some(),
1072 "{target} maps to no runner family"
1073 );
1074 }
1075 assert_eq!(runner_family("aarch64-unknown-linux-musl"), None);
1076 }
1077}
1078/// Public aggregate usage statistics served by `GET /api/v1/stats`.
1079///
1080/// Every count is derived from anonymized Analytics Engine events: hit
1081/// events are sampled at one in ten and their published counts are scaled
1082/// by the stored sample weight, so the numbers are approximate by design.
1083/// Fields that could publish a dangerously small count are suppressed
1084/// (`None`) rather than reported.
1085#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
1086pub struct UsageStats {
1087 /// Average number of distinct installs served a cache hit per day over
1088 /// the last 7 days, counted by daily-salted unlinkable install hash (a
1089 /// hash cannot be joined across days, so the figure is per-day, and
1090 /// hits are sampled, so it is a lower bound). `None` below the minimum
1091 /// publication threshold — stow never reports small counts.
1092 pub daily_active_installs_7d: Option<u64>,
1093 /// Cache hits served in the last 24 hours (sample-scaled estimate).
1094 pub hits_24h: u64,
1095 /// Cache misses served in the last 24 hours.
1096 pub misses_24h: u64,
1097 /// `hits_24h / (hits_24h + misses_24h)`; `0.0` when nothing was served.
1098 pub hit_rate_24h: f64,
1099 /// CPU-hours of rustc compilation saved in the last 30 days: the
1100 /// recorded compile time of every artifact served, sample-scaled.
1101 pub cpu_hours_saved_30d: f64,
1102 /// Most-served crates over the last 30 days.
1103 pub top_crates_30d: Vec<UsageStatEntry>,
1104 /// Hits per compilation target over the last 30 days.
1105 pub targets_30d: Vec<UsageStatEntry>,
1106 /// Hits per CLI version over the last 30 days; requests that sent no
1107 /// `stow-cli` user agent are not counted under any version.
1108 pub cli_versions_30d: Vec<UsageStatEntry>,
1109}
1110
1111/// One `(name, hits)` bucket of a [`UsageStats`] leaderboard.
1112#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
1113pub struct UsageStatEntry {
1114 /// Bucket label: crate name, target triple, or CLI version.
1115 pub name: String,
1116 /// Sample-scaled hit count for the bucket.
1117 pub hits: u64,
1118}