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