Skip to content

Commit 1973e40

Browse files
committed
fix(panel): resolve each bot's version from its own image repo
- Keep fleet reads off GitHub for release versions by returning the cached index immediately and spawning background refreshes instead of blocking on API calls - Support forked bot images by looking up release tags in the running image's repository rather than always using the configured default - Add per-repository release caching so different forks maintain independent version indexes - Implement faster retries (30s) for missing SHAs and failed fetches instead of the full 15-minute cache TTL - Track missed SHA lookups across retries to cap how long a never-released commit is queried - Wait up to 2 seconds for initial cache fill on bot detail/action pages so one-shot reads show proper versions - Extract release lookup logic with `release_lookup_image()`, `release_sha_hint()`, and `release_cache_key()` helpers for testability and clarity
1 parent df491d1 commit 1973e40

6 files changed

Lines changed: 432 additions & 61 deletions

File tree

.textile-monorepo-source

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
ee6836123285c636fa9fa51a68959316d61e7aa5
1+
9973cf47c734763cf7adecea909e900b04ee45df

.textile-stitch-release-version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.1.227
1+
0.1.228

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "stitch-bot"
3-
version = "0.1.227"
3+
version = "0.1.228"
44
edition = "2021"
55
description = "Stitch — Textile filler-network operator bot; quotes Swap via RFQ firm quotes and optionally fills resting limit orders."
66
license = "AGPL-3.0-or-later"

src/panel/http/bots.rs

Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -146,13 +146,12 @@ pub fn to_body(bot: &Bot, state: &AppState, fleet: &Fleet) -> BotBody {
146146
}
147147
}
148148

149-
async fn with_version(mut body: BotBody, bot: &Bot, state: &AppState) -> BotBody {
150-
body.version = running_version(state, bot).await;
149+
async fn with_version(mut body: BotBody, bot: &Bot, state: &AppState, wait: bool) -> BotBody {
150+
body.version = running_version(state, bot, wait).await;
151151
body
152152
}
153153

154-
async fn running_version(state: &AppState, bot: &Bot) -> Option<String> {
155-
let releases = crate::panel::versions::release_index(&state.cfg.bot_image).await;
154+
async fn running_version(state: &AppState, bot: &Bot, wait: bool) -> Option<String> {
156155
let lookup = bot
157156
.image_id
158157
.as_deref()
@@ -163,6 +162,35 @@ async fn running_version(state: &AppState, bot: &Bot) -> Option<String> {
163162
.local_image_labels(lookup)
164163
.await
165164
.unwrap_or_default();
165+
let repo_digests = if bot
166+
.image
167+
.as_deref()
168+
.is_none_or(crate::panel::updates::is_content_digest_ref)
169+
{
170+
state
171+
.docker
172+
.local_image_digests(lookup)
173+
.await
174+
.unwrap_or_default()
175+
} else {
176+
Vec::new()
177+
};
178+
let source = crate::panel::versions::release_lookup_image(
179+
bot.image.as_deref(),
180+
&repo_digests,
181+
&state.cfg.bot_image,
182+
);
183+
let needed_sha = crate::panel::versions::release_sha_hint(bot.image.as_deref(), &labels);
184+
let releases = if wait {
185+
crate::panel::versions::release_index_await(
186+
&source,
187+
needed_sha.as_deref(),
188+
crate::panel::versions::DETAIL_RELEASE_WAIT,
189+
)
190+
.await
191+
} else {
192+
crate::panel::versions::release_index(&source, needed_sha.as_deref())
193+
};
166194
crate::panel::versions::version_for_image(&releases, bot.image.as_deref(), &labels)
167195
}
168196

@@ -182,7 +210,7 @@ pub async fn list(State(state): State<AppState>) -> Result<Response, ApiError> {
182210
// fleet page stays alphabetical even if that ever changes.
183211
let mut bots = Vec::new();
184212
for b in fleet.bots() {
185-
bots.push(with_version(to_body(b, &state, &fleet), b, &state).await);
213+
bots.push(with_version(to_body(b, &state, &fleet), b, &state, false).await);
186214
}
187215
bots.sort_by(|a, b| a.name.cmp(&b.name));
188216
Ok(Json(FleetBody {
@@ -198,7 +226,7 @@ pub async fn show(
198226
Path(name): Path<String>,
199227
) -> Result<Response, ApiError> {
200228
let (bot, fleet) = state.bot_and_fleet(&name).await?;
201-
Ok(Json(with_version(to_body(&bot, &state, &fleet), &bot, &state).await).into_response())
229+
Ok(Json(with_version(to_body(&bot, &state, &fleet), &bot, &state, true).await).into_response())
202230
}
203231

204232
/// A lifecycle action's result. Carries the bot's new state so the UI doesn't
@@ -217,7 +245,7 @@ async fn action_response(
217245
) -> Result<Response, ApiError> {
218246
let (bot, fleet) = state.bot_and_fleet(name).await?;
219247
Ok(Json(ActionBody {
220-
bot: with_version(to_body(&bot, state, &fleet), &bot, state).await,
248+
bot: with_version(to_body(&bot, state, &fleet), &bot, state, true).await,
221249
message,
222250
})
223251
.into_response())
@@ -1703,7 +1731,7 @@ pub async fn migrate_layout(
17031731

17041732
let (fresh, fresh_fleet) = state.bot_and_fleet(&name).await?;
17051733
Ok(Json(serde_json::json!({
1706-
"bot": with_version(to_body(&fresh, &state, &fresh_fleet), &fresh, &state).await,
1734+
"bot": with_version(to_body(&fresh, &state, &fresh_fleet), &fresh, &state, true).await,
17071735
"message": report.message(),
17081736
"movedFiles": report.moved,
17091737
"ledgersRecovered": report.ledgers_recovered,
@@ -1867,6 +1895,33 @@ mod tests {
18671895
assert_eq!(v["version"], "v0.1.226", "{body}");
18681896
}
18691897

1898+
#[tokio::test]
1899+
async fn a_forked_bot_resolves_releases_from_its_own_repository() {
1900+
let h = harness("bot-version-fork");
1901+
seed_panel_bot(&h, "bot-a");
1902+
h.docker
1903+
.set_container_image("stitch-bot-a", "ghcr.io/someone/fork:sha-24e9192");
1904+
crate::panel::versions::set_test_release_index_for_repo(
1905+
"textile-protocol/textile-stitch",
1906+
std::collections::HashMap::from([("24e9192".into(), "v0.1.226".into())]),
1907+
);
1908+
crate::panel::versions::set_test_release_index_for_repo(
1909+
"someone/fork",
1910+
std::collections::HashMap::from([("24e9192".into(), "v9.9.9".into())]),
1911+
);
1912+
struct ResetReleases;
1913+
impl Drop for ResetReleases {
1914+
fn drop(&mut self) {
1915+
crate::panel::versions::clear_test_release_index();
1916+
}
1917+
}
1918+
let _reset = ResetReleases;
1919+
let (status, body) = h.get("/api/bots/bot-a").await;
1920+
assert_eq!(status, StatusCode::OK, "{body}");
1921+
let v = Harness::parse(&body);
1922+
assert_eq!(v["version"], "v9.9.9", "{body}");
1923+
}
1924+
18701925
#[tokio::test]
18711926
async fn an_empty_host_lists_no_bots() {
18721927
let h = harness("empty");

0 commit comments

Comments
 (0)