swarm-controller: answer 404, not 503, for an issue-report on a nonexistent repo

get_issue_report mapped every error from Client::issue_report to 503
(StatusUnavailable) unconditionally. issue_report calls
issue_list_issues(org, repo, ...) first, and a Forgejo 404 there — a
typo'd or deleted repo — was flattened into the same 503 a genuine
forge outage produces, which tells a client to retry a request that
will never succeed.

Downcast the anyhow error back to forgejo_api::ForgejoError (same
pattern main.rs's wanted_error_status uses for
swarm_queue_client::Error) and check it structurally against the
ApiErrorKind::NotFound / UnexpectedStatusCode(404) shapes forgejo's
generated client produces for a 404, rather than string-matching the
rendered message. Only that case answers 404, naming the org/repo;
every other forge failure still answers 503. Updates the route's
OpenAPI response list to document the 404.

Closes #4701
This commit is contained in:
atlas 2026-09-24 16:26:40 +02:00 • committed by mara
commit 244db367c4
2 changed files with 131 additions and 7 deletions

View file

@ -1103,6 +1103,27 @@ fn folds_into_success(e: &ForgejoError, existing: bool) -> bool {
is_confirmed_conflict(e) || (is_ambiguous_validation_failure(e) && existing)
}
/// Whether a forge call failed because the named repo itself doesn't exist —
/// forgejo answers a plain HTTP 404 for `owner/repo` not found, which the
/// generated client surfaces as `ApiErrorKind::NotFound`. Checked
/// defensively against `ApiErrorKind::Other`/`UnexpectedStatusCode` too,
/// same dual-check shape [`is_already_exists`] uses for 409/422 — belt and
/// braces against a response whose body didn't parse as the expected shape.
/// Used by [`crate::issue_report::get_issue_report`] to tell "no such repo"
/// (404, retrying won't help) apart from every other forge failure (503,
/// might clear up) — see that module for why the distinction matters.
pub(crate) fn is_repo_not_found(e: &ForgejoError) -> bool {
match e {
ForgejoError::ApiError(api) => match api.error_kind() {
ApiErrorKind::NotFound { .. } => true,
ApiErrorKind::Other(s) => *s == StatusCode::NOT_FOUND,
_ => false,
},
ForgejoError::UnexpectedStatusCode(s) => *s == StatusCode::NOT_FOUND,
_ => false,
}
}
#[cfg(test)]
fn validation_failed_error() -> ForgejoError {
ForgejoError::ApiError(ApiErrorKind::ValidationFailed.into())