fix
This commit is contained in:
+81
-10
@@ -73,10 +73,21 @@ fn head(repo: &Path) -> Option<String> {
|
||||
git(repo, &["rev-parse", "HEAD"])
|
||||
}
|
||||
|
||||
fn dirty(repo: &Path) -> bool {
|
||||
git(repo, &["status", "--porcelain"]).is_some_and(|s| !s.trim().is_empty())
|
||||
/// `shipped` limits the question to the paths an artifact is actually built from. It matters
|
||||
/// for the MCP: that repo also holds these release scripts, so an unscoped check let editing
|
||||
/// a script lock out every other step - the pack and the launcher included.
|
||||
fn dirty(repo: &Path, shipped: Option<&[&str]>) -> bool {
|
||||
let mut args = vec!["status", "--porcelain"];
|
||||
if let Some(paths) = shipped {
|
||||
args.push("--");
|
||||
args.extend_from_slice(paths);
|
||||
}
|
||||
git(repo, &args).is_some_and(|s| !s.trim().is_empty())
|
||||
}
|
||||
|
||||
/// What lands in the MCP image: `Dockerfile` copies only these.
|
||||
const MCP_SHIPPED: &[&str] = &["Dockerfile", "requirements.txt", "src"];
|
||||
|
||||
/// One artifact's row: is this repo past what is published?
|
||||
fn item(
|
||||
key: &'static str,
|
||||
@@ -84,10 +95,11 @@ fn item(
|
||||
repo: PathBuf,
|
||||
live: Option<String>,
|
||||
published_commit: Option<String>,
|
||||
shipped: Option<&[&str]>,
|
||||
) -> ReleaseItem {
|
||||
let exists = repo.is_dir();
|
||||
let commit = exists.then(|| head(&repo)).flatten();
|
||||
let dirty = exists && dirty(&repo);
|
||||
let dirty = exists && dirty(&repo, shipped);
|
||||
let behind = match (&commit, &published_commit) {
|
||||
(Some(now), Some(was)) => Some(now != was),
|
||||
_ => None,
|
||||
@@ -118,18 +130,30 @@ pub fn plan(
|
||||
let scripts = mcp_repo.join("scripts").join("release_pack.ps1").is_file()
|
||||
&& mcp_repo.join("scripts").join("package_launcher.ps1").is_file();
|
||||
let items = vec![
|
||||
item("pack", "ODIAN", dev_root.join("ODIAN_Engine"), pack_live, pack_commit),
|
||||
item("launcher", "Launcher", dev_root.join("ODIAN_Launcher"), launcher_live, launcher_commit),
|
||||
item("pack", "ODIAN", dev_root.join("ODIAN_Engine"), pack_live, pack_commit, None),
|
||||
item("launcher", "Launcher", dev_root.join("ODIAN_Launcher"), launcher_live, launcher_commit, None),
|
||||
// The container is built from the same Engine checkout as the pack; only its pipeline
|
||||
// differs, and only it needs Docker.
|
||||
item("api", "Engine API", dev_root.join("ODIAN_Engine"), api_live, None),
|
||||
item("api", "Engine API", dev_root.join("ODIAN_Engine"), api_live, None, None),
|
||||
item("mcp", "MCP server", dev_root.join("ODIAN_MCP_Server"), None, None, Some(MCP_SHIPPED)),
|
||||
];
|
||||
let blocked = if !online {
|
||||
Some("The DHP is not answering: nothing can be published.".to_string())
|
||||
} else if !scripts {
|
||||
Some(format!("The release scripts are not in {}.", mcp_repo.display()))
|
||||
} else if items.iter().any(|i| i.dirty) {
|
||||
Some("Commit what is open first: a release built from uncommitted work cannot be rebuilt.".to_string())
|
||||
// Naming them matters: "pack" and "api" are the same checkout, so a bare message sent
|
||||
// you hunting through three repos for one open file.
|
||||
let mut who: Vec<&str> = Vec::new();
|
||||
for i in items.iter().filter(|i| i.dirty) {
|
||||
if !who.contains(&i.title) {
|
||||
who.push(i.title);
|
||||
}
|
||||
}
|
||||
Some(format!(
|
||||
"Commit what is open first ({}): a release built from uncommitted work cannot be rebuilt.",
|
||||
who.join(", ")
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -152,6 +176,7 @@ pub fn run_release(
|
||||
pack_version: &str,
|
||||
launcher_version: Option<&str>,
|
||||
with_api: bool,
|
||||
with_mcp: bool,
|
||||
sudo: &str,
|
||||
) -> Result<PathBuf, String> {
|
||||
let scripts = mcp_repo.join("scripts");
|
||||
@@ -189,6 +214,19 @@ pub fn run_release(
|
||||
));
|
||||
}
|
||||
|
||||
if with_mcp {
|
||||
// LAST, always. The MCP serves /download/* and proxies /dhp, so rolling it interrupts
|
||||
// the very road the steps above publish over; anything still in flight would fail.
|
||||
body.push_str(&format!(
|
||||
"Write-Host '=== MCP server ===' -ForegroundColor Cyan
|
||||
& '{}'
|
||||
if ($LASTEXITCODE -eq 2) {{ Write-Host 'MCP: nothing to deploy' -ForegroundColor Yellow }}
|
||||
elseif ($LASTEXITCODE -ne 0) {{ throw 'the MCP step failed' }}
|
||||
",
|
||||
scripts.join("deploy_mcp.ps1").display()
|
||||
));
|
||||
}
|
||||
|
||||
// Everything inside try/finally, and everything recorded. A step that throws used to
|
||||
// take the window with it - the release simply vanished, with nothing to read.
|
||||
let ps = format!(
|
||||
@@ -279,17 +317,50 @@ mod tests {
|
||||
fn a_repo_that_matches_what_is_published_is_not_behind() {
|
||||
// The comparison is COMMIT against COMMIT. Versions cannot answer it: a version says
|
||||
// what was cut, not from what - which is how a release gets cut twice from one tree.
|
||||
let same = item("pack", "ODIAN", PathBuf::from("."), Some("0.71.0".into()), head(Path::new(".")));
|
||||
let same = item("pack", "ODIAN", PathBuf::from("."), Some("0.71.0".into()), head(Path::new(".")), None);
|
||||
if same.commit.is_some() {
|
||||
assert_eq!(same.behind, Some(false), "same commit: nothing to release");
|
||||
}
|
||||
// Nothing published says where it came from: say so rather than guess.
|
||||
let unknown = item("pack", "ODIAN", PathBuf::from("."), Some("0.71.0".into()), None);
|
||||
let unknown = item("pack", "ODIAN", PathBuf::from("."), Some("0.71.0".into()), None, None);
|
||||
assert_eq!(unknown.behind, None);
|
||||
assert!(unknown.note.contains("does not say") || unknown.dirty || unknown.commit.is_none());
|
||||
// A folder that is not there is named, not silently "up to date".
|
||||
let missing = item("pack", "ODIAN", PathBuf::from(r"Z:\nothing\here"), None, None);
|
||||
let missing = item("pack", "ODIAN", PathBuf::from(r"Z:\nothing\here"), None, None, None);
|
||||
assert_eq!(missing.behind, None);
|
||||
assert!(missing.note.contains("not in the checkout folder"));
|
||||
}
|
||||
|
||||
/// The regression that adding the MCP step introduced: that repo also holds the release
|
||||
/// scripts, so an unscoped `git status` made editing a script look like uncommitted
|
||||
/// release content and locked out the pack and the launcher too.
|
||||
#[test]
|
||||
fn a_dirty_file_outside_the_shipped_paths_does_not_block() {
|
||||
let dir = std::env::temp_dir().join(format!("odian-dirty-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(dir.join("src")).unwrap();
|
||||
std::fs::create_dir_all(dir.join("scripts")).unwrap();
|
||||
let ok = |args: &[&str]| git(&dir, args).is_some();
|
||||
if !ok(&["init"]) {
|
||||
return; // no git here; nothing to prove
|
||||
}
|
||||
let _ = git(&dir, &["config", "user.email", "t@t"]);
|
||||
let _ = git(&dir, &["config", "user.name", "t"]);
|
||||
std::fs::write(dir.join("src").join("app.py"), "x").unwrap();
|
||||
std::fs::write(dir.join("requirements.txt"), "y").unwrap();
|
||||
std::fs::write(dir.join("Dockerfile"), "FROM x").unwrap();
|
||||
assert!(ok(&["add", "-A"]) && ok(&["commit", "-m", "base"]));
|
||||
assert!(!dirty(&dir, Some(MCP_SHIPPED)), "clean tree");
|
||||
|
||||
// An untracked release script: tooling, never in the image.
|
||||
std::fs::write(dir.join("scripts").join("deploy_mcp.ps1"), "# tool").unwrap();
|
||||
assert!(dirty(&dir, None), "the repo really is dirty");
|
||||
assert!(!dirty(&dir, Some(MCP_SHIPPED)), "but nothing that ships changed");
|
||||
|
||||
// Something that does ship: this must still block.
|
||||
std::fs::write(dir.join("src").join("app.py"), "changed").unwrap();
|
||||
assert!(dirty(&dir, Some(MCP_SHIPPED)), "shipped source changed: block");
|
||||
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user