~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/knotless

git that lives in your Atmosphere account

knotless.dev
1 collaborator

knotless 0.13.0 Latest 2 packages, 1 image

1
10

A push takes a batch's paths skip at its tree once its base passed

Merged chadtmiller.com asked to merge into main from chadtmiller.com/knotless:run/reuse-path-skips
crates/knotless-run/src/lib.rs+33−3
@@ -2104,6 +2104,8 @@ impl Runner {
check.skipped = Some(why);
check.finished_at = Some(now());
self.reporter.put(&watch.remote, &rkey, &check).await?;
// A push at the same tree takes it, once its base passed.
self.keep_passed(watch, &subject, &wf, &rkey)?;
continue;
}
self.reporter.put(&watch.remote, &rkey, &check).await?;
@@ -2442,6 +2444,11 @@ impl Runner {
let Some(earlier) = earlier.filter(|c| c.status == status::SUCCESS) else {
return Ok(false);
};
// One its paths left out read nothing that changed from its base:
// it stands for the push only once the base's own run passed.
if earlier.skipped.is_some() && !self.passed_on(watch, p, &earlier, wf).await? {
return Ok(false);
}
let uri = format!(
"at://{}/{}/{}",
self.reporter.identity.did,
@@ -2466,15 +2473,38 @@ impl Runner {
check.status = status::SUCCESS.into();
check.steps = earlier.steps;
check.outputs = earlier.outputs;
// Its log copies the earlier one's, which may be pruned.
let log = earlier.log.as_ref().context("its run has no log")?;
let before = self.reporter.log_of(&watch.remote, log).await;
// Its log copies the earlier one's, which may be pruned; one its
// paths left out has none.
let before = match &earlier.log {
Some(log) => self.reporter.log_of(&watch.remote, log).await,
None => Ok(earlier
.skipped
.as_ref()
.map(|why| format!("==> {why}\n").into_bytes())
.unwrap_or_default()),
};
let log = check::copied(&line, &before.unwrap_or_default());
check.log = Some(self.reporter.upload_log(&log).await?);
check.finished_at = Some(now());
Ok(true)
}
/// Whether `wf` passed on `p`'s base branch at the commit `earlier`, a
/// run there that `p` keeps, was checked on: a push's own run there.
async fn passed_on(
&self,
watch: &Watch,
p: &Passed,
earlier: &Check,
wf: &Workflow,
) -> Result<bool> {
let base = earlier.base_commit.as_deref().unwrap_or_default();
let (owner, key) = (watch.remote.did(), &watch.key);
let rkey = check::rkey(owner, key, &p.base, base, None, &wf.name);
let there = self.reporter.get(&watch.remote, &rkey).await?;
Ok(there.is_some_and(|c| c.status == status::SUCCESS))
}
/// Waits for what a run ranked `rank` holds while its steps go, or
/// `None` if `cancelled` first. One of a workflow whose runs finish
/// first has its turn on its ref or pull, `on_ref`, and is kept from
crates/knotless-run/tests/run.rs+36−0
@@ -2354,6 +2354,12 @@ fn a_merged_pull_passes_its_push() {
"deploy",
"on: {push: {refs: [refs/heads/main]}}\nsuperseded: finish\n".to_string(),
),
// Run on a pull only when it changes web/.
(
"web",
"on: {push: {refs: [refs/heads/main]}, pull: {base: [refs/heads/main], paths: [web/**]}}\n"
.to_string(),
),
] {
let text = format!(
"{extra}steps:\n - name: note\n run: |\n echo \"$KNOTLESS_RUN_SHA\" >> {go}/{wf}\n test ! -f {go}/break\n",
@@ -2362,6 +2368,7 @@ fn a_merged_pull_passes_its_push() {
let text = match wf {
"ci" => text + &warm,
"secret" => text + " - name: token\n run: echo \"token=$TOKEN\"\n",
"web" => text + &format!(" - name: web\n run: test ! -f {}/webbreak\n", go.display()),
_ => text,
};
std::fs::write(dir.join(format!("{wf}.yml")), text).unwrap();
@@ -2606,6 +2613,35 @@ fn a_merged_pull_passes_its_push() {
assert_eq!(ci["status"], "success", "{ci}");
assert!(ran("ci").contains(&forced), "{}", ran("ci"));
assert!(!log_of(&ci).contains("not run again"));
// A pull its paths leave web out of, whose base passed web, passes
// the push at its tree without running it.
assert_eq!(at(&forced, "web")["status"], "success");
let (plain, _) = open("plain", "p");
let skipped = check_on(&runner, &owner, &key, &plain, "web");
assert!(skipped["skipped"].is_string(), "{skipped}");
let landed = merged(&["merge", &url, &plain]);
let web = at(&landed, "web");
assert_eq!(web["status"], "success", "{web}");
assert!(!ran("web").contains(&landed), "{}", ran("web"));
let log = log_of(&web);
assert!(log.contains("; not run again\n"), "{log}");
assert!(
log.contains("==> skipped: nothing changed in web/**"),
"{log}"
);
// One whose base failed web runs it.
std::fs::write(go.join("webbreak"), "").unwrap();
let broke = owner.commit_file(&work, "g", "g\n");
owner.git(&work, &["push", "-q", "origin", "main"]);
pass();
assert_eq!(at(&broke, "web")["status"], "failure");
std::fs::remove_file(go.join("webbreak")).unwrap();
let (after, _) = open("after", "h");
let ran_again = merged(&["merge", &url, &after]);
let web = at(&ran_again, "web");
assert_eq!(web["status"], "success", "{web}");
assert!(ran("web").contains(&ran_again), "{}", ran("web"));
}
#[test]
docs/design/runner.md+8−0
@@ -854,6 +854,14 @@ A pass is not reused when:
- the run left a step out for its ref (`on`), which the push, at another
ref, may run.
A pull's or batch's check that its `paths` left out passed without
running, as nothing it reads changed from its base. The push at its tree
takes it too, but only once the push's own run of that workflow passed
at that base: what the skip reads is then what passed there. Without
that, a push to main would run every workflow a batch skipped, though it
checked that very tree. Its log says so, and that the run it took was
skipped.
A batch can likewise take a trusted pull's pass at the same tree. Passes
stay in the state file after their cache layers are forgotten, so a check
can still be reused without its caches.