Diffusion: a two-half pill, upstream and downstream 73ef9f767132
A consumer's status is drawn as a capsule: the upstream on the left (checked, untested, failing, testing) and the consumer on the right (passes, fails, testing, waiting, up to date), each half coloured on its own. A game that passes on an untested upstream reads [untested | passes] instead of a single amber that hid the pass. refs #150
- Author
- Andy McCall @andymccall <[email protected]>
- When
- · 2026-10-13 11:30 UTC
- Signature
- ✔ signed by Andy McCall
- Hash
73ef9f7671328214d75c86253d0e1094580d7cace048c6d44716d68728d58fde- Depends on
- 180dc43ad0e0 757195ad7a5d 8620925e5c53 93be9ce4653e cc19697f8871 d23464c0ac60 f677d6270298
- Depended on by
- 737de0fac722 a10446c68043 5d90e0e045e2 eb2a3502601d c9aacc0e7dc1 58f915792b85
- In bookmarks
- api-unknown-fields cancel-landed-runs diffusion-pin-bookmark draft-ready main oh-comment-body-file proposal-list-fast
- In tags
- v0.5.0 v0.5.1 v0.5.2 v0.6.0 v0.6.1 v0.7.0 v0.8.0 v0.9.0 v0.9.1 v0.9.11 v0.9.15 v0.9.16 v0.9.19 v0.9.2 v0.9.20 v0.9.21 v0.9.22 v0.9.23 v0.9.24 v0.9.3 v0.9.4 v0.9.5 v0.9.6 v0.9.7 v0.9.8 v0.9.9
- Edits
- 22 atoms, 0 tree operations, 8 files
Browse the tree at this change
modified crates/hub/src/diffusion.rs +56 −1
| … | 157 unchanged lines |
||
| 158 | 158 | pub landed: Option<(i64, i64)>, |
|
| 159 | 159 | /// What the views show (#150). |
|
| 160 | 160 | pub colour: Colour, |
|
| 161 | + | /// The pill's two halves: the upstream, then this consumer. |
|
| 162 | + | pub upstream: Half, |
|
| 163 | + | pub downstream: Half, |
|
| 161 | 164 | } |
|
| 162 | 165 | ||
| 166 | + | /// One half of a Diffusion pill: a colour class and a word (#150). |
|
| 167 | + | #[derive(Clone, Debug, PartialEq, Eq, Serialize)] |
|
| 168 | + | pub struct Half { |
|
| 169 | + | pub class: &'static str, |
|
| 170 | + | pub word: &'static str, |
|
| 171 | + | pub title: String, |
|
| 172 | + | } |
|
| 173 | + | ||
| 174 | + | impl Half { |
|
| 175 | + | /// The upstream's half: was the upstream itself tested? |
|
| 176 | + | pub fn upstream(unverified: bool, run: Option<&str>, slug: &str) -> Half { |
|
| 177 | + | let (class, word, title) = match (unverified, run) { |
|
| 178 | + | (true, _) => ("df-amber", "untested", format!("{slug} has no checks of its own: nothing has tested it")), |
|
| 179 | + | (false, Some("passed")) => ("df-green", "checked", format!("{slug}'s newest run passed")), |
|
| 180 | + | (false, Some("failed" | "error" | "cancelled")) => ("df-red", "failing", format!("{slug}'s newest run failed")), |
|
| 181 | + | (false, _) => ("df-grey", "testing", format!("{slug}'s newest run has not finished")), |
|
| 182 | + | }; |
|
| 183 | + | Half { class, word, title } |
|
| 184 | + | } |
|
| 185 | + | ||
| 186 | + | /// The consumer's half: does it pass with the upstream? |
|
| 187 | + | pub fn downstream(follow: Follow, behind: Option<usize>, bump: Option<i64>, run: Option<&str>, name: &str) -> Half { |
|
| 188 | + | let (class, word, title) = if follow == Follow::Off { |
|
| 189 | + | ("df-off", "not following", format!("{name} does not follow it")) |
|
| 190 | + | } else { |
|
| 191 | + | match (bump, run) { |
|
| 192 | + | (Some(_), Some("passed")) => ("df-green", "passes", format!("{name}'s run on the bump passed")), |
|
| 193 | + | (Some(_), Some("failed" | "error" | "cancelled")) => ("df-red", "fails", format!("{name}'s run on the bump failed")), |
|
| 194 | + | (Some(_), Some(_)) => ("df-grey", "testing", format!("{name}'s run on the bump has not finished")), |
|
| 195 | + | (Some(_), None) => ("df-grey", "waiting", format!("{name}'s bump has no run yet")), |
|
| 196 | + | (None, _) if behind == Some(0) => ("df-green", "up to date", format!("{name} pins the newest state")), |
|
| 197 | + | (None, _) => ("df-grey", "waiting", format!("{name} is behind; its bump has not been made yet")), |
|
| 198 | + | } |
|
| 199 | + | }; |
|
| 200 | + | Half { class, word, title } |
|
| 201 | + | } |
|
| 202 | + | } |
|
| 203 | + | ||
| 163 | 204 | /// A consumer's Diffusion status at a glance (#150, Andy's colours): |
|
| 164 | 205 | /// green when the upstream was checked and the consumer's run on the bump |
|
| 165 | 206 | /// passed; amber when the run passed but the upstream has no checks; red |
|
| … | 105 unchanged lines |
||
| 271 | 312 | run, |
|
| 272 | 313 | landed, |
|
| 273 | 314 | colour: Colour::Off, |
|
| 315 | + | upstream: Half { class: "df-grey", word: "", title: String::new() }, |
|
| 316 | + | downstream: Half { class: "df-grey", word: "", title: String::new() }, |
|
| 274 | 317 | }); |
|
| 275 | 318 | } |
|
| 276 | 319 | if out.is_empty() { |
|
| … | 11 unchanged lines |
||
| 288 | 331 | .collect::<Vec<_>>()) |
|
| 289 | 332 | }) |
|
| 290 | 333 | .await?; |
|
| 334 | + | let up_run: Option<String> = |
|
| 335 | + | sqlx::query_scalar("SELECT status FROM flow_runs WHERE repo_id = ? AND bookmark = ? ORDER BY id DESC LIMIT 1") |
|
| 336 | + | .bind(row.id) |
|
| 337 | + | .bind(&row.default_bookmark) |
|
| 338 | + | .fetch_optional(&app.db) |
|
| 339 | + | .await |
|
| 340 | + | .ok() |
|
| 341 | + | .flatten(); |
|
| 342 | + | let slug = format!("{}/{}", row.owner, row.name); |
|
| 291 | 343 | for (c, b) in out.iter_mut().zip(behind) { |
|
| 292 | 344 | c.behind = b; |
|
| 293 | - | c.colour = Colour::of(c.follow, b, c.bump, c.run.as_ref().map(|r| r.1.as_str()), unverified); |
|
| 345 | + | let run = c.run.as_ref().map(|r| r.1.clone()); |
|
| 346 | + | c.colour = Colour::of(c.follow, b, c.bump, run.as_deref(), unverified); |
|
| 347 | + | c.upstream = Half::upstream(unverified, up_run.as_deref(), &slug); |
|
| 348 | + | c.downstream = Half::downstream(c.follow, b, c.bump, run.as_deref(), &format!("{}/{}", c.owner, c.name)); |
|
| 294 | 349 | } |
|
| 295 | 350 | Ok(out) |
|
| 296 | 351 | } |
|
| … | 743 unchanged lines |
modified docs/diffusion.md +7 −0
| … | 116 unchanged lines |
||
| 117 | 117 | failed), **grey** (running), **up to date**. The overview shows the same in |
|
| 118 | 118 | brief; Admin › Diffusion shows the whole hub. |
|
| 119 | 119 | ||
| 120 | + | It is drawn as a two-half pill, like a capsule (Andy, 2026-10-13): the |
|
| 121 | + | left half is the upstream (**checked**, **untested**, **failing**, |
|
| 122 | + | **testing**), the right half the consumer (**passes**, **fails**, |
|
| 123 | + | **testing**, **waiting**, **up to date**), each coloured on its own, so a |
|
| 124 | + | game that passes on an untested upstream reads [untested │ passes] rather |
|
| 125 | + | than a single amber that hides the pass. |
|
| 126 | + | ||
| 120 | 127 | ## 5. Across hubs |
|
| 121 | 128 | ||
| 122 | 129 | Each hub only ever writes to its own repositories. What one hub tells |
|
| … | 100 unchanged lines |
modified docs/spec.md +1 −1
| … | 514 unchanged lines |
||
| 515 | 515 | - its key is the one the runner's token first signed with. The first signed receipt under a token binds that token to its key in `flow_runner_keys`. |
|
| 516 | 516 | ||
| 517 | 517 | Otherwise the job's result still stands, and the run page says why the receipt was not kept. The run page links each kept receipt, with its signer. The change page lists the receipts for the state whose only head is that change. `/{owner}/{name}/receipts/{attest}` downloads a signed receipt for `oxy receipt verify` or `import`. |
|
| 518 | - | - **Architecture** (OxygenFlow spec §11.2–§11.3, oxygenflow#36): a runner reports each image's architecture on its claims (`image_archs`), kept in `flow_images`; a job is offered only to a runner whose architecture matches its sandbox's `arch`, else its image's as known; unknown matches, as before. The queue says which architecture a waiting job asks for. **Images by hash** (oxygenflow#39, Jetstream): `flow rootfs push HASH` sends a rootfs image as the FastCDC chunks the hub lacks (`POST /flow/images/missing`, `PUT /flow/images/chunks/{hash}`, by a token that may write repositories) and then its manifest (`PUT /flow/images/{hash}`), which the hub accepts only when every chunk is held and they make an image with that hash; chunks are kept zstd-compressed under `<data>/images`, shared by every image, and `jet_images` lists what is held. Runners and accounts fetch with `GET /flow/images/{hash}` and `GET /flow/images/chunks/{hash}`. A runner whose claims say `fetches_images` is offered jobs whose image only the hub holds, and fetches it before its preflight. Each claim also carries the runner's `facts` (OxygenFlow spec §11.1, oxygenflow#40): free disk, load, speed score and class, kernel; the hub keeps the latest on the runner, shows them on Admin › Runners and in `GET /runners`. A job's finish carries the most disk and memory it took (`peak_disk_mib`, `peak_memory_mib`, oxygenflow#41); a new job's `need_disk_mib` is the most its last five runs of the same repository and name took, plus a quarter and 64 MiB, and it is offered only to a runner whose claim reports at least that much free disk; the waiting reason says so. **Ranking** (oxygenflow#42): a job that another free runner which polled in the last 10 s would do better (it holds the job's image while the claimant would fetch it, or has a higher speed class) is held from the claimant for up to 15 s, then any runner that fits takes it; each job records why it ran where it did (`placed`: "holds the image, idle, speed 100 (class 3)", with "after N s held for a better runner" when it was), shown on the run page, in the API and by `oh run view`. A run whose jobs pin an image held by neither the hub nor a runner sets a failing `pipeline/images` check on its state (#137), naming the images and how to send them (`flow rootfs push`), so a proposal pinning a new image shows it before it lands; a later run of the state turns it to passed once every image is held. A placement pass every 15 seconds works out why each ready queued job no runner can take waits, from what each online runner offered at its last claim (scope, tier, architecture, image held, labels, memory and CPUs, a recent decline), and shows it on the job (API `waiting`, `oh run view` and `oh run watch`, its panel open on the run page) and in its check's summary, so a proposal says why its check is stuck (#28). **Diffusion** (#148, `docs/diffusion.md`): a push that moves a repository's default bookmark, or the bookmark its work lands on (the most common target of its proposals landed in the last 90 days), records each Oxygen subrepo pin in the new state (`diffusion_pins`: consumer, bookmark, path, the upstream's Oxygen repository id, the pinned heads); a hub that had no index fills it at start. `GET /repos/{owner}/{name}/consumers` lists the consumers the caller may read, each with its pin and how many changes of this repository's default bookmark it lacks (`behind`, null when the pin names changes this repository does not hold), and the overview shows them as Used by. A repository that pins, or is pinned by, another on the hub has a **Diffusion tab** (#150): Used by (each consumer's pin bookmark, how far behind, its status, its open bump and that bump's newest run, its last landed bump, how it follows) and Follows (each upstream it pins, with the same); its overview shows both in brief, and Admin › Diffusion lists every upstream with its consumers. The status is green when the upstream is checked and the consumer's run on the bump passed, amber when that run passed but the upstream has no checks, red when the run failed, grey while it runs, and up to date when there is nothing to bump. Landing a proposal on the hub refreshes the pin index and propagates as a push does. A bump's run, started by the `diffusion` account, is given the upstream it bumps as a subrepo whatever that account may read (the consumer's own pin of it was committed by someone who may write the consumer); its other subrepos need read access as usual. A consumer follows a subrepo's upstream as its `.oxymodules` entry says (`follow = off | green | any`, `land = propose | auto`, `target` naming the bookmark to bump, `watch` listing paths whose upstream changes are flagged), else as its repository setting says (Settings › Diffusion, or `PUT /repos/{owner}/{name}/diffusion {"follow": ...}` by its owners and admins; migration 0058), else as `[diffusion] follow` in hub.toml says (default `off`). A **bump** moves a consumer's pin at a path to a newer upstream state: one change on the target bookmark's heads that replaces only the pin (oxy-core `build_pin_change`), signed with the hub's key, on the bookmark `diffusion/<path>`, which the hub owns and rebuilds on the target each time (a forced update). When the pin has changes the upstream's state lacks (say, from an upstream proposal not landed yet), the bump moves to both, so a consumer never loses changes it relies on; it is skipped when that combination would conflict, when the pin names changes the upstream does not hold, when the pin already holds the state, or when the bump bookmark already moves it there, and the hub logs why. A bump is **proposed** once per bump bookmark, by the hub's `diffusion` account (made on first use, never able to sign in), into the target, labelled `dependencies`, its body the upstream changes the pin lacks (newest first, linking to the upstream's change pages); a later bump edits the same open proposal. It is triggered when a push moves the upstream's default bookmark (for consumers following `any`) or when a run of that bookmark passes (for `green`). Diffusion never lands a proposal. An upstream with no checks of its own (Flow off, or no run ever of its default bookmark) is **unverified**: it never turns green, so its moves also bump consumers following `green`, and those proposals are marked amber with the `upstream unverified` label and a note in the body; a later bump from a passing run removes the label. A bump already in place refreshes its proposal's text and labels. A consumer's `watch` patterns (`*` within a path segment, `**` across them, `?` one character; a plain path covers everything below it) flag the upstream changes touching a matching file: they are listed first under "Touches what you watch", with the files, and marked ⚑ in the changelog. Turning Diffusion on for a consumer (the setting or the API) catches it up at once: it is bumped to each upstream's newest state whose default-bookmark run passed, or to its newest state when it follows every move. **Images page** (#146): Admin › Runners › Images lists every rootfs image a runner holds, the hub keeps, or a job ran on in the last 90 days (from each job's sandbox), newest use first: its size and architecture, where it is (the hub, each runner holding it, each runner fetching it from the swarm now), the repositories whose jobs used it, and who sent it; an image held nowhere is marked. **The swarm** (#144, oxygenflow docs/jetstream.md §8.1): a runner in an image swarm reports its peer address, its site and the BLAKE3 of its TLS certificate with each claim (`swarm`; without a certificate it is not in the swarm), kept in memory and shown on Admin › Runners. `POST /flow/swarm/announce {image, state}` (by a runner; `fetching`, `done` or `stopped`) returns the online runners in its site that hold the image or announced fetching it in the last 45 seconds, each with its certificate's fingerprint, which the fetcher pins its TLS connection to, and a ticket signed with the hub key (JSON naming the runner, its site and an expiry 15 minutes on, hex, then the signature) that peers check against `/.well-known/oxyhub-key` before serving chunks; a runner that has not reported an address is told to add `[runner.swarm]` (409). |
|
| 518 | + | - **Architecture** (OxygenFlow spec §11.2–§11.3, oxygenflow#36): a runner reports each image's architecture on its claims (`image_archs`), kept in `flow_images`; a job is offered only to a runner whose architecture matches its sandbox's `arch`, else its image's as known; unknown matches, as before. The queue says which architecture a waiting job asks for. **Images by hash** (oxygenflow#39, Jetstream): `flow rootfs push HASH` sends a rootfs image as the FastCDC chunks the hub lacks (`POST /flow/images/missing`, `PUT /flow/images/chunks/{hash}`, by a token that may write repositories) and then its manifest (`PUT /flow/images/{hash}`), which the hub accepts only when every chunk is held and they make an image with that hash; chunks are kept zstd-compressed under `<data>/images`, shared by every image, and `jet_images` lists what is held. Runners and accounts fetch with `GET /flow/images/{hash}` and `GET /flow/images/chunks/{hash}`. A runner whose claims say `fetches_images` is offered jobs whose image only the hub holds, and fetches it before its preflight. Each claim also carries the runner's `facts` (OxygenFlow spec §11.1, oxygenflow#40): free disk, load, speed score and class, kernel; the hub keeps the latest on the runner, shows them on Admin › Runners and in `GET /runners`. A job's finish carries the most disk and memory it took (`peak_disk_mib`, `peak_memory_mib`, oxygenflow#41); a new job's `need_disk_mib` is the most its last five runs of the same repository and name took, plus a quarter and 64 MiB, and it is offered only to a runner whose claim reports at least that much free disk; the waiting reason says so. **Ranking** (oxygenflow#42): a job that another free runner which polled in the last 10 s would do better (it holds the job's image while the claimant would fetch it, or has a higher speed class) is held from the claimant for up to 15 s, then any runner that fits takes it; each job records why it ran where it did (`placed`: "holds the image, idle, speed 100 (class 3)", with "after N s held for a better runner" when it was), shown on the run page, in the API and by `oh run view`. A run whose jobs pin an image held by neither the hub nor a runner sets a failing `pipeline/images` check on its state (#137), naming the images and how to send them (`flow rootfs push`), so a proposal pinning a new image shows it before it lands; a later run of the state turns it to passed once every image is held. A placement pass every 15 seconds works out why each ready queued job no runner can take waits, from what each online runner offered at its last claim (scope, tier, architecture, image held, labels, memory and CPUs, a recent decline), and shows it on the job (API `waiting`, `oh run view` and `oh run watch`, its panel open on the run page) and in its check's summary, so a proposal says why its check is stuck (#28). **Diffusion** (#148, `docs/diffusion.md`): a push that moves a repository's default bookmark, or the bookmark its work lands on (the most common target of its proposals landed in the last 90 days), records each Oxygen subrepo pin in the new state (`diffusion_pins`: consumer, bookmark, path, the upstream's Oxygen repository id, the pinned heads); a hub that had no index fills it at start. `GET /repos/{owner}/{name}/consumers` lists the consumers the caller may read, each with its pin and how many changes of this repository's default bookmark it lacks (`behind`, null when the pin names changes this repository does not hold), and the overview shows them as Used by. A repository that pins, or is pinned by, another on the hub has a **Diffusion tab** (#150): Used by (each consumer's pin bookmark, how far behind, its status, its open bump and that bump's newest run, its last landed bump, how it follows) and Follows (each upstream it pins, with the same); its overview shows both in brief, and Admin › Diffusion lists every upstream with its consumers. Each consumer's status is a two-half pill: the upstream on the left (checked when its newest default-bookmark run passed, untested when it has no checks, failing, or testing) and the consumer on the right (passes or fails for its run on the bump, testing, waiting, or up to date), each half coloured green, amber, red or grey; the API keeps one overall `colour` as well. Landing a proposal on the hub refreshes the pin index and propagates as a push does. A bump's run, started by the `diffusion` account, is given the upstream it bumps as a subrepo whatever that account may read (the consumer's own pin of it was committed by someone who may write the consumer); its other subrepos need read access as usual. A consumer follows a subrepo's upstream as its `.oxymodules` entry says (`follow = off | green | any`, `land = propose | auto`, `target` naming the bookmark to bump, `watch` listing paths whose upstream changes are flagged), else as its repository setting says (Settings › Diffusion, or `PUT /repos/{owner}/{name}/diffusion {"follow": ...}` by its owners and admins; migration 0058), else as `[diffusion] follow` in hub.toml says (default `off`). A **bump** moves a consumer's pin at a path to a newer upstream state: one change on the target bookmark's heads that replaces only the pin (oxy-core `build_pin_change`), signed with the hub's key, on the bookmark `diffusion/<path>`, which the hub owns and rebuilds on the target each time (a forced update). When the pin has changes the upstream's state lacks (say, from an upstream proposal not landed yet), the bump moves to both, so a consumer never loses changes it relies on; it is skipped when that combination would conflict, when the pin names changes the upstream does not hold, when the pin already holds the state, or when the bump bookmark already moves it there, and the hub logs why. A bump is **proposed** once per bump bookmark, by the hub's `diffusion` account (made on first use, never able to sign in), into the target, labelled `dependencies`, its body the upstream changes the pin lacks (newest first, linking to the upstream's change pages); a later bump edits the same open proposal. It is triggered when a push moves the upstream's default bookmark (for consumers following `any`) or when a run of that bookmark passes (for `green`). Diffusion never lands a proposal. An upstream with no checks of its own (Flow off, or no run ever of its default bookmark) is **unverified**: it never turns green, so its moves also bump consumers following `green`, and those proposals are marked amber with the `upstream unverified` label and a note in the body; a later bump from a passing run removes the label. A bump already in place refreshes its proposal's text and labels. A consumer's `watch` patterns (`*` within a path segment, `**` across them, `?` one character; a plain path covers everything below it) flag the upstream changes touching a matching file: they are listed first under "Touches what you watch", with the files, and marked ⚑ in the changelog. Turning Diffusion on for a consumer (the setting or the API) catches it up at once: it is bumped to each upstream's newest state whose default-bookmark run passed, or to its newest state when it follows every move. **Images page** (#146): Admin › Runners › Images lists every rootfs image a runner holds, the hub keeps, or a job ran on in the last 90 days (from each job's sandbox), newest use first: its size and architecture, where it is (the hub, each runner holding it, each runner fetching it from the swarm now), the repositories whose jobs used it, and who sent it; an image held nowhere is marked. **The swarm** (#144, oxygenflow docs/jetstream.md §8.1): a runner in an image swarm reports its peer address, its site and the BLAKE3 of its TLS certificate with each claim (`swarm`; without a certificate it is not in the swarm), kept in memory and shown on Admin › Runners. `POST /flow/swarm/announce {image, state}` (by a runner; `fetching`, `done` or `stopped`) returns the online runners in its site that hold the image or announced fetching it in the last 45 seconds, each with its certificate's fingerprint, which the fetcher pins its TLS connection to, and a ticket signed with the hub key (JSON naming the runner, its site and an expiry 15 minutes on, hex, then the signature) that peers check against `/.well-known/oxyhub-key` before serving chunks; a runner that has not reported an address is told to add `[runner.swarm]` (409). |
|
| 519 | 519 | - **Variables** (oxygenhub#102): Administration, an organisation's settings and a repository's settings each hold `NAME=value` pairs (a name is a letter or `_` then letters, digits and `_`, up to 64, never `FLOW_*`; a value up to 4096 characters; up to 100 per level) under **Variables**, each page showing what it inherits and which inherited names it overrides. A claim carries the repository's effective set as `env`, inner levels overriding outer; Flow puts them in the job's environment under `FLOW_*` and the pipeline's own `env`, so they are part of the receipt. They are not secrets and are shown in full to whoever sees the page; secrets are mounted by Flow (OxygenFlow spec §9), never put in the environment. Changes are audited (`flow.variable.set`, `flow.variable.deleted`); `flow run --var NAME=VALUE` gives a local run the same. |
|
| 520 | 520 | - **Secrets** (oxygenhub#107; OxygenFlow spec §9): the same three levels hold **Secrets**, named as a pipeline's `secret "name" { mount-path "/run/secrets/…" }` names them (1 to 64 of `A-Z a-z 0-9 . _ -`, a value up to 64 KiB, up to 100 per level), typed once and sealed under `keys/secrets.key`; pages show names, when each was set and where it comes from, never a value. A claim carries only the secrets its job declares, each with its path and value, from the innermost level that sets the name; one no level sets is noted at the top of the job's log and left out, so the job fails on the missing file rather than running with a stand-in. Flow writes each as a file (0400, the job's user's, on an in-memory mount) before the job starts and never puts it in the environment, the receipt or an artifact. Log intake replaces each value, and its base64, with `***` before a record is stored. Changes are audited (`flow.secret.set`, `flow.secret.deleted`), never with the value. |
|
| 521 | 521 | - **Artifacts** (OxygenFlow spec §5.2): a job may declare artifacts with `output`. |
|
| … | 231 unchanged lines |
modified templates/admin_diffusion.html +2 −2
| … | 3 unchanged lines |
||
| 4 | 4 | {% block mainclass %} class="wide"{% endblock %} |
|
| 5 | 5 | {% block content %} |
|
| 6 | 6 | <div class="head"><h1>Diffusion <small>{{ upstreams.len()|plural("upstream", "upstreams") }} · <a href="/admin">Administration</a></small></h1></div> |
|
| 7 | - | <p class="small mute df-key"><span class="df-chip df-green">green</span> checked upstream, passed in the consumer · <span class="df-chip df-amber">amber</span> passed, upstream has no checks · <span class="df-chip df-red">red</span> the consumer's run failed · <span class="df-chip df-grey">testing</span> running · <span class="df-chip df-current">up to date</span></p> |
|
| 7 | + | <p class="small mute df-key">Each pill has two halves. Left, the upstream: <span class="df-pill"><span class="df-half df-green">checked</span></span> its own run passed, <span class="df-pill"><span class="df-half df-amber">untested</span></span> it has no checks, <span class="df-pill"><span class="df-half df-red">failing</span></span> its run failed. Right, the repository that pins it: <span class="df-pill"><span class="df-half df-green">passes</span></span> its run on the bump passed, <span class="df-pill"><span class="df-half df-red">fails</span></span> it failed, <span class="df-pill"><span class="df-half df-grey">testing</span></span> still running, <span class="df-pill"><span class="df-half df-green">up to date</span></span> nothing to bump.</p> |
|
| 8 | 8 | {% if upstreams.is_empty() %}<p class="empty">No repository on this hub is pinned by another yet.</p>{% endif %} |
|
| 9 | 9 | {% for (up, list, unverified) in upstreams %} |
|
| 10 | - | <h2><a href="/{{ up.owner }}/{{ up.name }}/diffusion">{{ up.owner }}/{{ up.name }}</a> <small>{{ list.len()|plural("consumer", "consumers") }}{% if *unverified %} · <span class="df-chip df-amber">no checks of its own</span>{% endif %}</small></h2> |
|
| 10 | + | <h2><a href="/{{ up.owner }}/{{ up.name }}/diffusion">{{ up.owner }}/{{ up.name }}</a> <small>{{ list.len()|plural("consumer", "consumers") }}{% if *unverified %} · <span class="df-pill"><span class="df-half df-amber">untested</span></span>{% endif %}</small></h2> |
|
| 11 | 11 | {% call d::rows(list) %} |
|
| 12 | 12 | {% endfor %} |
|
| 13 | 13 | {% endblock %} |
modified templates/diffusion.html +2 −2
| … | 5 unchanged lines |
||
| 6 | 6 | {% block tabs %}{% call m::tabs(repo, "diffusion") %}{% endblock %} |
|
| 7 | 7 | {% block content %} |
|
| 8 | 8 | <div class="head"><h1>Diffusion <small>how changes move between this repository and the ones that pin it</small></h1></div> |
|
| 9 | - | <p class="small mute df-key"><span class="df-chip df-green">green</span> checked here and passed there · <span class="df-chip df-amber">amber</span> passed there, but the upstream has no checks · <span class="df-chip df-red">red</span> its run on the bump failed · <span class="df-chip df-grey">testing</span> running · <span class="df-chip df-current">up to date</span> nothing to bump</p> |
|
| 9 | + | <p class="small mute df-key">Each pill has two halves. Left, the upstream: <span class="df-pill"><span class="df-half df-green">checked</span></span> its own run passed, <span class="df-pill"><span class="df-half df-amber">untested</span></span> it has no checks, <span class="df-pill"><span class="df-half df-red">failing</span></span> its run failed. Right, the repository that pins it: <span class="df-pill"><span class="df-half df-green">passes</span></span> its run on the bump passed, <span class="df-pill"><span class="df-half df-red">fails</span></span> it failed, <span class="df-pill"><span class="df-half df-grey">testing</span></span> still running, <span class="df-pill"><span class="df-half df-green">up to date</span></span> nothing to bump.</p> |
|
| 10 | 10 | {% if !used_by.is_empty() %} |
|
| 11 | 11 | <h2>Used by <span class="count-chip">{{ used_by.len() }}</span></h2> |
|
| 12 | 12 | {% if unverified %}<div class="strip warn">{{ repo.owner }}/{{ repo.name }} has no checks of its own, so its consumers' bumps are amber: only their own pipelines test them. A <code>.flow/pipeline.kdl</code> here that passes turns them green.</div>{% endif %} |
|
| … | 2 unchanged lines |
||
| 15 | 15 | {% if !follows.is_empty() %} |
|
| 16 | 16 | <h2>Follows <span class="count-chip">{{ follows.len() }}</span></h2> |
|
| 17 | 17 | <div class="table"><table class="diffusion"> |
|
| 18 | - | <tr><th>Upstream</th><th>Subrepo</th><th>Behind</th><th>Status</th><th>Open bump</th><th>Last landed</th></tr> |
|
| 18 | + | <tr><th>Upstream</th><th>Subrepo</th><th>Behind</th><th>Upstream │ downstream</th><th>Open bump</th><th>Last landed</th></tr> |
|
| 19 | 19 | {% for (up, c) in follows %} |
|
| 20 | 20 | <tr> |
|
| 21 | 21 | <td><a href="/{{ up.owner }}/{{ up.name }}/diffusion">{{ up.owner }}/{{ up.name }}</a></td> |
|
| … | 12 unchanged lines |
modified templates/diffusion_rows.html +2 −2
| 1 | - | {% macro chip(c) %}<span class="df-chip {{ c.colour.class() }}" title="{{ c.colour.label() }}">{{ c.colour.label() }}</span>{% endmacro %} |
|
| 1 | + | {% macro chip(c) %}<span class="df-pill" role="img" aria-label="upstream {{ c.upstream.word }}, downstream {{ c.downstream.word }}"><span class="df-half {{ c.upstream.class }}" title="Upstream: {{ c.upstream.title }}">{{ c.upstream.word }}</span><span class="df-half {{ c.downstream.class }}" title="Downstream: {{ c.downstream.title }}">{{ c.downstream.word }}</span></span>{% endmacro %} |
|
| 2 | 2 | ||
| 3 | 3 | {% macro rows(list) %} |
|
| 4 | 4 | <div class="table"><table class="diffusion"> |
|
| 5 | - | <tr><th>Repository</th><th>Pin on</th><th>Behind</th><th>Status</th><th>Open bump</th><th>Last landed</th><th>Follows</th></tr> |
|
| 5 | + | <tr><th>Repository</th><th>Pin on</th><th>Behind</th><th>Upstream │ downstream</th><th>Open bump</th><th>Last landed</th><th>Follows</th></tr> |
|
| 6 | 6 | {% for c in list %} |
|
| 7 | 7 | <tr> |
|
| 8 | 8 | <td><a href="/{{ c.owner }}/{{ c.name }}">{{ c.owner }}/{{ c.name }}</a> <span class="small mute">{{ c.path }}</span></td> |
|
| … | 10 unchanged lines |
modified templates/overview.html +2 −2
| … | 60 unchanged lines |
||
| 61 | 61 | <ul class="plain">{% for b in o.bookmarks %}<li><a href="{{ repo.url }}/tree/{{ b.name }}">{{ b.name }}</a>{% if b.heads.len() > 1 %} <span class="tag warn">{{ b.heads.len() }} heads</span>{% endif %}{% if b.conflicted %} <span class="tag bad">conflicts</span>{% endif %}</li>{% endfor %}</ul> |
|
| 62 | 62 | {% if let Some(rel) = latest_release %}<h3>Latest release</h3><p><a href="{{ repo.url }}/releases/tag/{{ rel.tag }}">{{ rel.title }}</a> <span class="small mute">{{ rel.created_ms|ago_i }}</span></p>{% endif %} |
|
| 63 | 63 | {% if !used_by.is_empty() %}<h3>Used by <span class="count-chip">{{ used_by.len() }}</span></h3> |
|
| 64 | - | <ul class="plain used-by">{% for c in used_by %}<li><a href="/{{ c.owner }}/{{ c.name }}">{{ c.owner }}/{{ c.name }}</a> <span class="df-chip {{ c.colour.class() }}" title="{{ c.colour.label() }}">{% match c.behind %}{% when Some(0) %}up to date{% when Some(n) %}{{ n }} behind{% when None %}?{% endmatch %}</span></li>{% endfor %}</ul> |
|
| 64 | + | <ul class="plain used-by">{% for c in used_by %}<li><a href="/{{ c.owner }}/{{ c.name }}">{{ c.owner }}/{{ c.name }}</a> <span class="df-pill"><span class="df-half {{ c.upstream.class }}" title="Upstream: {{ c.upstream.title }}">{{ c.upstream.word }}</span><span class="df-half {{ c.downstream.class }}" title="Downstream: {{ c.downstream.title }}">{% match c.behind %}{% when Some(0) %}up to date{% when Some(n) %}{{ c.downstream.word }} · {{ n }} behind{% when None %}{{ c.downstream.word }}{% endmatch %}</span></span></li>{% endfor %}</ul> |
|
| 65 | 65 | <p class="small"><a href="{{ repo.url }}/diffusion">Diffusion ›</a></p>{% endif %} |
|
| 66 | 66 | {% if !follows.is_empty() %}<h3>Follows</h3> |
|
| 67 | - | <ul class="plain used-by">{% for (up, c) in follows %}<li><a href="/{{ up.owner }}/{{ up.name }}">{{ up.owner }}/{{ up.name }}</a> <span class="df-chip {{ c.colour.class() }}" title="{{ c.colour.label() }}">{{ c.colour.label() }}</span>{% if let Some(n) = c.bump %} <a class="small" href="{{ repo.url }}/proposal/{{ n }}">#{{ n }}</a>{% endif %}</li>{% endfor %}</ul> |
|
| 67 | + | <ul class="plain used-by">{% for (up, c) in follows %}<li><a href="/{{ up.owner }}/{{ up.name }}">{{ up.owner }}/{{ up.name }}</a> <span class="df-pill" role="img" aria-label="upstream {{ c.upstream.word }}, downstream {{ c.downstream.word }}"><span class="df-half {{ c.upstream.class }}" title="Upstream: {{ c.upstream.title }}">{{ c.upstream.word }}</span><span class="df-half {{ c.downstream.class }}" title="Downstream: {{ c.downstream.title }}">{{ c.downstream.word }}</span></span>{% if let Some(n) = c.bump %} <a class="small" href="{{ repo.url }}/proposal/{{ n }}">#{{ n }}</a>{% endif %}</li>{% endfor %}</ul> |
|
| 68 | 68 | <p class="small"><a href="{{ repo.url }}/diffusion">Diffusion ›</a></p>{% endif %} |
|
| 69 | 69 | <h3>Tags</h3> |
|
| 70 | 70 | {% if o.tags.is_empty() %}<p class="mute small">None.</p>{% else %}<ul class="plain">{% for t in o.tags %}<li><a href="{{ repo.url }}/tree/{{ t.name }}">{{ t.name }}</a>{% if t.signer.is_some() %} <span class="sig sig-valid" title="signed">✔</span>{% endif %}</li>{% endfor %}</ul>{% endif %} |
|
| … | 10 unchanged lines |
modified templates/style.css +11 −0
| … | 1004 unchanged lines |
||
| 1005 | 1005 | .df-off { color: var(--dim); border-color: var(--line); border-style: dashed; } |
|
| 1006 | 1006 | .df-key .df-chip { margin-right: 2px; } |
|
| 1007 | 1007 | table.diffusion td { vertical-align: middle; } |
|
| 1008 | + | ||
| 1009 | + | /* The Diffusion pill (#150): two halves like a capsule, the upstream on |
|
| 1010 | + | the left, the repository that pins it on the right. */ |
|
| 1011 | + | .df-pill { display: inline-flex; border-radius: 999px; overflow: hidden; font-size: 12px; line-height: 1.6; vertical-align: middle; box-shadow: inset 0 0 0 1px var(--line); white-space: nowrap; } |
|
| 1012 | + | .df-half { padding: 0 9px; font-weight: 600; } |
|
| 1013 | + | .df-half + .df-half { border-left: 1px solid rgba(255, 255, 255, .55); } |
|
| 1014 | + | .df-half.df-green { background: #1a7f4b; color: #fff; } |
|
| 1015 | + | .df-half.df-amber { background: #e3a008; color: #2b1d00; } |
|
| 1016 | + | .df-half.df-red { background: #c62d47; color: #fff; } |
|
| 1017 | + | .df-half.df-grey { background: var(--soft); color: var(--mute); } |
|
| 1018 | + | .df-half.df-off { background: transparent; color: var(--dim); } |