diff --git a/swarm-controller/src/forge.rs b/swarm-controller/src/forge.rs index 17753d21..a21fa422 100644 --- a/swarm-controller/src/forge.rs +++ b/swarm-controller/src/forge.rs @@ -414,7 +414,33 @@ impl Client { /// check [`Self::seed_agent_config`] uses instead of catching a /// create-conflict, since `repo_change_files` has no /// already-exists-is-fine status the way `create_org_repo` does. + /// + /// `create_repo` makes `repo` with `auto_init: false` (see + /// [`Self::repo_option`]) — no commits, no `main` ref, nothing a + /// contents lookup can resolve. Forgejo doesn't 404 that the way it + /// does a missing path on a real ref: it returns `200` with a bare + /// JSON `[]`, which fails to deserialize into the single-object + /// `ContentsResponse` the crate expects for a file path ("invalid + /// length 0, expected struct `ContentsResponse` with 16 elements" — + /// serde reading the 0-element array as a positional struct). So a + /// freshly created, not-yet-seeded repo trips this as an opaque + /// deserialize error, not the `NOT_FOUND` branch below — confirmed + /// against a live re-test of the swarm agent-create flow tracked on + /// the forge. Short-circuit on `empty` first: an empty repo + /// obviously has no `agent.nix`, and skipping + /// the contents call for it sidesteps the malformed-response shape + /// entirely rather than trying to special-case parsing it. async fn file_exists(&self, repo: &str, path: &str) -> Result { + let is_empty = self + .api + .repo_get(CONFIG_ORG, repo) + .await + .with_context(|| format!("get repo {CONFIG_ORG}/{repo}"))? + .empty + .unwrap_or(false); + if is_empty { + return Ok(false); + } match self .api .repo_get_contents(CONFIG_ORG, repo, path, RepoGetContentsQuery::default())