chadtmiller.com/knotless
git that lives in your Atmosphere account
1
10
A push takes a batch's paths skip at its tree once its base passed
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. | ||