johnandersen777.bsky.social/knotless
git that lives in your Atmosphere account
Forked from chadtmiller.com/knotless
0
0
//! 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/<name>` 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<App>) {
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<Value> {
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::<Vec<_>>(),
"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<Shared> {
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());
}
}