chadtmiller.com/knotless
git that lives in your Atmosphere account
1
10
A release not made says why, rather than always that it exists
Only what this pull adds. A release can be yanked, leaving it to read but no one's latest landed as a59f04e, so its changes do not show here again.
.changeset/release-published-already.mdnew+7−0
| @@ -0,0 +1,7 @@ | ||
--- | ||
default: patch | ||
--- | ||
| ||
# A release that fails to publish says why | ||
| ||
Publishing a version now says it is published already only when it is. Any other failure, such as a login that was refused, gives its own reason. | ||
crates/knotless/src/release.rs+13−4
| @@ -252,10 +252,19 @@ impl Target { | ||
}; | ||
let typed = records::typed(nsid::RELEASE, &record); | ||
let write = remote::create(nsid::RELEASE, &self.rkey, typed); | ||
// A publish that raced this one to the version made the record first. | ||
let made = self.remote.apply(vec![write], auth).await; | ||
made.context(self.published())?; | ||
Ok(self.uri.clone()) | ||
let Err(failed) = self.remote.apply(vec![write], auth).await else { | ||
return Ok(self.uri.clone()); | ||
}; | ||
// A publish that raced this one to the version made the record | ||
// first. A PDS need not say so, as the reference PDS's plain 500 | ||
// does not, so only the record tells that from any other failure. | ||
let found = self | ||
.remote | ||
.get_record::<ReleaseRecord>(nsid::RELEASE, &self.rkey); | ||
match found.await? { | ||
Some(_) => bail!(self.published()), | ||
None => Err(failed), | ||
} | ||
} | ||
} | ||
crates/knotless-serve/tests/releases.rs+53−0
| @@ -750,6 +750,59 @@ fn pushing_an_image() { | ||
assert_eq!(Pushing::code(p.manifest(&image_digest, &image)).0, 502); | ||
} | ||
| ||
// A lost race is told apart from any other failed write, which says what | ||
// went wrong rather than that the version was published. | ||
#[test] | ||
fn a_release_not_made_says_why() { | ||
use knotless::push; | ||
use knotless::records::{RELEASE_MIME, ReleaseFile, ReleasePart}; | ||
use knotless::release::{Made, Target}; | ||
use knotless::url::RepoUrl; | ||
use knotless::xrpc::Auth; | ||
| ||
let mut s = Sandbox::new(); | ||
let handle = s.account.handle.clone(); | ||
s.set("KNOTLESS_IDENTIFIER", &handle); | ||
let url = s.url("raced"); | ||
let work = s.work("w", 1); | ||
s.git(&work, &["push", "-q", &url, "main"]); | ||
let ctx = ctx(&s); | ||
let url = RepoUrl::parse(&url).unwrap(); | ||
let rt = tokio::runtime::Runtime::new().unwrap(); | ||
rt.block_on(async { | ||
let session = knotless::session::create(&ctx, &handle, &s.account.password); | ||
let auth = session.await.unwrap().auth(); | ||
let target = Target::open(&ctx, &url, "tool", "1.0.0").await.unwrap(); | ||
let blobs = push::upload(&ctx, &target.remote, &auth, b"x", RELEASE_MIME); | ||
let file = ReleaseFile { | ||
name: "x".into(), | ||
size: 1, | ||
sha256: knotless::release::sha256(b"x"), | ||
parts: blobs | ||
.await | ||
.unwrap() | ||
.into_iter() | ||
.map(|blob| ReleasePart { blob }) | ||
.collect(), | ||
}; | ||
let stranger = Auth::Bearer("no such token".into()); | ||
let create = |auth| target.create(auth, vec![file.clone()], Made::default()); | ||
| ||
let refused = create(&stranger).await.unwrap_err(); | ||
assert!( | ||
!format!("{refused:#}").contains("published already"), | ||
"{refused:#}" | ||
); | ||
| ||
create(&auth).await.unwrap(); | ||
let lost = create(&auth).await.unwrap_err(); | ||
assert!( | ||
format!("{lost:#}").contains("tool 1.0.0 is published already"), | ||
"{lost:#}" | ||
); | ||
}); | ||
} | ||
| ||
/// An appview handing out the releases in `dir`, whose describeServer | ||
/// answers what `described` holds. | ||
fn appview(dir: &Path, described: Arc<Mutex<serde_json::Value>>) -> Server { | ||