//! What repositories share for others to use, as the index keeps it //! (`index::shared`): the files of their releases, read and kept as what //! they are, and which repositories use each. //! //! A shared step is a release of the package `@step/` whose one //! file is a step file, published with the commit it was made from, as //! `knotless workflow pin` takes one. Its file is read once, from the //! release's blobs on its owner's PDS, checked against its record and kept //! ([`Records`]), as a release's files never change. One that is not a //! step file, or names no commit, is kept as not one, and never shown. //! //! Who uses a step is read from `.knotless/uses.lock` on each public //! repository's default branch, as what it is written in is counted //! ([`crate::languages::follow`]): once a list shows it, and again as its //! refs move. A repository is counted once for each step it pins, at //! whatever versions. use crate::App; use crate::index::{Index, STEP, Shared}; use crate::releases::Records; use knotless::url::RepoUrl; use knotless::uses::{self, Use}; use serde_json::{Value, json}; use std::sync::Arc; /// Reads the files of the releases of things shared as the index asks /// ([`Index::reading`]): first those it kept before and has yet to read, /// then each as it is indexed, one at a time. pub async fn follow(app: Arc) { let taken = app.index.clone().and_then(|i| Some((i.reading()?, i))); let Some((mut asked, index)) = taken else { return; }; for uri in index.unread().unwrap_or_default() { read(&app, &index, &uri).await; } while let Some(uri) = asked.recv().await { read(&app, &index, &uri).await; } } /// Reads the file of the release at `uri` and keeps what it is. One that /// cannot be read now, its owner's PDS away, is left to be read when /// next the server starts. async fn read(app: &App, index: &Index, uri: &str) -> Option<()> { let (thing, release) = index.shared_release(uri).ok().flatten()?; let name = index.name(&thing.did, &thing.key).ok().flatten()?; let found = match (&release.files[..], &release.commit) { ([file], Some(_)) => { let url = RepoUrl::parse(&format!("knotless://{}/{name}", thing.did)).ok()?; let records = Records::new(app.ctx.clone(), url, release.package.clone()); let bytes = records.read(file).await.ok()?; step(&file.name, &bytes) } _ => None, }; index.set_shared(uri, found.as_ref()).ok() } /// The step file `bytes`, named `file`, as the index keeps it: what it /// does, takes and asks for. None for one that is not a step file. pub fn step(file: &str, bytes: &[u8]) -> Option { let parsed = uses::parse_step(file, bytes).ok()?; let inputs = parsed.inputs.iter().map(|(name, input)| { let mut shown = json!({"name": name, "required": input.required}); if !input.description.is_empty() { shown["description"] = input.description.clone().into(); } if let Some(default) = &input.default { shown["default"] = default.clone().into(); } shown }); let description = Some(parsed.description).filter(|d| !d.trim().is_empty()); Some(json!({ "description": description, "inputs": inputs.collect::>(), "needs": parsed.needs, "secrets": parsed.secrets, })) } /// Keeps what `did`'s repository with `key` uses, from `lock`, the lock /// file on its default branch: nothing, with none, or one that does not /// parse. A step whose repository the index does not have is passed by, /// for a count after it does. pub async fn read_uses(app: &App, index: &Index, did: &str, key: &str, lock: Option<&[u8]>) { let lock = lock.and_then(|text| uses::parse_lock(text).ok()); let mut used = Vec::new(); for named in lock.unwrap_or_default().keys() { if let Some(thing) = step_of(app, index, named).await { used.push(thing); } } let _ = index.set_uses(did, key, &used); } /// The step a lock's `uses` names, if the index has its repository. async fn step_of(app: &App, index: &Index, named: &str) -> Option { let used = Use::parse(named).ok()?; let owner = knotless::identity::resolve(&app.ctx, &used.owner).await; let did = owner.ok()?.did; let key = index.key_named(&did, &used.repo).ok().flatten()?; Some(Shared { kind: STEP.into(), did, key, name: used.step, }) } #[cfg(test)] mod tests { use super::*; #[test] fn a_step_file_is_kept_as_what_it_takes_and_asks_for() { let file = b"description: Waits.\ninputs:\n workflows: {description: Which, required: true}\n minutes: {default: \"25\"}\nneeds: [curl]\nsecrets: [TOKEN]\nsteps:\n - run: echo\n"; let kept = step("wait.yml", file).unwrap(); assert_eq!( kept, json!({ "description": "Waits.", "inputs": [ {"name": "minutes", "required": false, "default": "25"}, {"name": "workflows", "required": true, "description": "Which"}, ], "needs": ["curl"], "secrets": ["TOKEN"], }) ); assert_eq!( step("x.yml", b"steps: []\n").unwrap()["description"], Value::Null ); assert!(step("x.yml", b"not: a step\n").is_none()); } }