fix(#2375): pr_is_open state check at submission + atomic fetched_sha INSERT

Two hardening items from argus's review of #2374:

1. PR state check at submission:
   - Add `pr_is_open(repo, pr)` to forge/pr_merge.rs using
     `repo_get_pull_request` + `StateType` — early error if the PR is
     already closed or merged instead of queuing a card that fails later
   - Call it in `submit_merge_config_pr` before fetching the head sha

2. Atomic fetched_sha INSERT:
   - Add `fetched_sha: Option<&str>` to `Approvals::submit_kind` so
     the sha can be included in the INSERT rather than a follow-up UPDATE
   - MergeConfigPr already knows the sha before inserting the row
     (pr_head_sha runs first) → pass `Some(&sha)`, drop the separate
     `set_fetched_sha` call → truly atomic
   - ApplyCommit still needs two writes (sha resolved by git_fetch_to_tag
     after the row exists) → pass `None`, `set_fetched_sha` unchanged
   - All other callers (InitConfig, Spawn, UpdateMetaInputs,
     SchedulePrompt) pass `None` — no behavioural change
   - Add `fetched_sha_in_insert_is_readable_via_get` test covering the
     MergeConfigPr path
This commit is contained in:
atlas 2026-07-11 10:27:20 +02:00 committed by mara
commit 96eda4ed6b
7 changed files with 102 additions and 15 deletions

View file

@ -10,7 +10,7 @@ mod users;
pub use pr_merge::{
ForgeMergeError, config_repo, fetch_pr_head_into_applied, ff_push_to_main, mark_pr_merged,
pr_head_sha,
pr_head_sha, pr_is_open,
};
pub use repos::{
create_agent_repo, ensure_config_repo, ensure_knowledge_repo, ensure_meta_remote, ensure_repo,

View file

@ -5,7 +5,7 @@
use anyhow::Context;
use forgejo_api::ForgejoError;
use forgejo_api::structs::{MergePullRequestOption, MergePullRequestOptionDo};
use forgejo_api::structs::{MergePullRequestOption, MergePullRequestOptionDo, StateType};
use super::{CONFIG_ORG, api, core_token, forge_git_url};
@ -112,6 +112,30 @@ pub async fn pr_head_sha(repo: &str, pr: u64) -> Result<String, ForgeMergeError>
Ok(sha.to_string())
}
/// Check whether PR `pr` on `repo` is still open. Returns `Ok(true)` if
/// open, `Ok(false)` if closed or merged, or an error on transport failure.
///
/// Called at submission time to give an early, actionable error rather than
/// queuing an approval card that will fail later in the approve handler.
///
/// # Errors
/// `Other` on transport failure or a missing/malformed PR response.
pub async fn pr_is_open(repo: &str, pr: u64) -> Result<bool, ForgeMergeError> {
let token = core_token()
.ok_or_else(|| ForgeMergeError::Other(anyhow::anyhow!("forge core token absent")))?;
let (owner, name) = repo.split_once('/').ok_or_else(|| {
ForgeMergeError::Other(anyhow::anyhow!("forge repo `{repo}` is not owner/name"))
})?;
let index = i64::try_from(pr)
.map_err(|_| ForgeMergeError::Other(anyhow::anyhow!("PR index {pr} overflows i64")))?;
let client = api(&token).map_err(ForgeMergeError::Other)?;
let pull = client
.repo_get_pull_request(owner, name, index)
.await
.map_err(|e| ForgeMergeError::Other(anyhow::Error::from(e).context("GET pull request")))?;
Ok(pull.state == Some(StateType::Open))
}
/// Full `owner/name` path of an agent's config repo on the forge — the
/// `agent-configs` org mirror that the PR-merge flow reads + fast-forwards.
pub fn config_repo(agent: &str) -> String {