feat(merge): gate publication on the merged tree's own tests
The other half of the merge button. Merging told you the branch went in; nothing
checked that what came out still worked.
Verified BEFORE publishing, not reverted after. `merge_locally` and
`push_merged` are separate functions so the caller can run the project's tests
between them, which means a merge that breaks the base is simply never pushed —
`main` is not broken for however long it takes someone to notice. A test asserts
`merge_locally` contains no push, because the moment it does, verification
becomes after-the-fact and the guarantee is gone.
Outcomes, all reported to the operator rather than swallowed:
Passed -> published
NoSuite -> published, and SAID so; a repo with no tests is a fact about the
repo, not a pass
Failed -> not published, exit code reported, branch untouched so it can be
fixed and merged again
CouldNotRun -> not published. Fail closed: a suite that could not run has not
passed, and publishing on "we could not check" is how a green
main stops meaning anything.
`verify_tests` runs `cargo test` as ROOT in a container, so the merge workdir
ends up holding a root-owned `target/` the server (uid 65532) cannot delete —
the same leak found three times today. Purged through the container before the
ordinary cleanup.
248 lib tests.
This commit is contained in:
@@ -469,8 +469,61 @@ pub async fn merge_branch(
|
||||
})));
|
||||
}
|
||||
|
||||
let outcome =
|
||||
crate::auto_merge::merge_on_operator_approval(&workdir, &auth.url, &branch, base).await;
|
||||
let container = std::env::var("CLAWMATES_RUNTIME_CONTAINER")
|
||||
.unwrap_or_else(|_| "clawmates-runtime".to_string());
|
||||
let outcome = async {
|
||||
let merged =
|
||||
crate::auto_merge::merge_on_operator_approval(&workdir, &auth.url, &branch, base)
|
||||
.await?;
|
||||
if !merged.merged {
|
||||
return Ok(merged);
|
||||
}
|
||||
|
||||
// Run the project's own tests against the MERGED tree, before it is
|
||||
// published. Verifying first rather than reverting after is the
|
||||
// difference between "main was never broken" and "main was broken until
|
||||
// someone noticed".
|
||||
//
|
||||
// The merge is already committed locally at this point; refusing here
|
||||
// simply never pushes it, and the branch is still there to retry.
|
||||
match crate::mission_delivery::verify_tests(&workdir, &container).await {
|
||||
crate::mission_delivery::TestOutcome::Passed => {}
|
||||
crate::mission_delivery::TestOutcome::NoSuite => {
|
||||
eprintln!(
|
||||
"missions::merge_branch: {branch} has no discoverable test suite — publishing unverified"
|
||||
);
|
||||
}
|
||||
crate::mission_delivery::TestOutcome::Failed(code) => {
|
||||
return Ok(crate::auto_merge::MergeOutcome {
|
||||
merged: false,
|
||||
reason: format!(
|
||||
"the merged tree FAILS the project's tests (exit {code}) — not published. The branch is unchanged; fix it and merge again."
|
||||
),
|
||||
});
|
||||
}
|
||||
// Fail closed. A suite that could not run has not passed, and
|
||||
// publishing on "we could not check" is how a green main stops
|
||||
// meaning anything.
|
||||
crate::mission_delivery::TestOutcome::CouldNotRun(why) => {
|
||||
return Ok(crate::auto_merge::MergeOutcome {
|
||||
merged: false,
|
||||
reason: format!("could not run the tests on the merged tree ({why}) — not published"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
crate::auto_merge::push_merged(&workdir, &auth.url, base).await?;
|
||||
Ok::<_, String>(crate::auto_merge::MergeOutcome {
|
||||
merged: true,
|
||||
reason: format!("tests pass on the merged tree; published to {base}"),
|
||||
})
|
||||
}
|
||||
.await;
|
||||
|
||||
// Purge through the container: `verify_tests` runs `cargo test` as ROOT, so
|
||||
// the workdir now holds a root-owned `target/` the server (uid 65532) cannot
|
||||
// delete. Same defect as the bench and judge copies.
|
||||
crate::root_copy::purge(&container, &workdir).await;
|
||||
let _ = tokio::fs::remove_dir_all(&workdir).await;
|
||||
|
||||
match outcome {
|
||||
|
||||
Reference in New Issue
Block a user