Skip to content

Commit df491d1

Browse files
committed
feat: show release versions instead of image SHAs
- Add `version` field to `BotBody` to display GitHub release tags (e.g. `v0.1.226`) instead of Docker image SHAs - Implement `release_index()` to fetch GitHub release metadata from GHCR repositories and map commit SHAs to version tags - Implement `version_for_image()` to resolve running images to their release version by checking image tags, SHA lookups, and OCI labels - Add `is_release_tag()` validator to identify semver release tags and distinguish them from channel tags - Populate version field for bot list, detail, and action endpoints via new `with_version()` helper - Update frontend `VersionRollback` component to display release versions as primary labels with image tags as fallback - Add `imageLabel()` utility to show release version when available, otherwise shortened Docker ref - Update `BotVersion` type to include `version` field for rollback UI - Cache GitHub release metadata separately from commit metadata for efficient fleet page queries - Add test coverage for release version resolution and OCI label fallbacks
1 parent 24e9192 commit df491d1

11 files changed

Lines changed: 373 additions & 22 deletions

File tree

.textile-monorepo-source

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

.textile-stitch-release-version

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

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.226"
3+
version = "0.1.227"
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: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ pub struct BotBody {
4444
/// frontend never has to keep its own list of Docker states in sync.
4545
pub can_stop: bool,
4646
pub image: Option<String>,
47+
/// GitHub release for the running image (`v0.1.226`), when one can be
48+
/// attributed. The image field stays the Docker ref; the UI shows this.
49+
pub version: Option<String>,
4750
pub created_unix: Option<i64>,
4851
/// Whether the panel can edit this bot's config at all.
4952
pub editable: bool,
@@ -130,6 +133,7 @@ pub fn to_body(bot: &Bot, state: &AppState, fleet: &Fleet) -> BotBody {
130133
running: bot.state.is_running(),
131134
can_stop: bot.container_name.is_some() && !bot.state.is_terminal(),
132135
image: bot.image.clone(),
136+
version: None,
133137
created_unix: bot.created_unix,
134138
editable: bot.is_editable(),
135139
can_migrate: migrate_check.is_ok(),
@@ -142,6 +146,26 @@ pub fn to_body(bot: &Bot, state: &AppState, fleet: &Fleet) -> BotBody {
142146
}
143147
}
144148

149+
async fn with_version(mut body: BotBody, bot: &Bot, state: &AppState) -> BotBody {
150+
body.version = running_version(state, bot).await;
151+
body
152+
}
153+
154+
async fn running_version(state: &AppState, bot: &Bot) -> Option<String> {
155+
let releases = crate::panel::versions::release_index(&state.cfg.bot_image).await;
156+
let lookup = bot
157+
.image_id
158+
.as_deref()
159+
.filter(|id| !id.is_empty())
160+
.or(bot.image.as_deref())?;
161+
let labels = state
162+
.docker
163+
.local_image_labels(lookup)
164+
.await
165+
.unwrap_or_default();
166+
crate::panel::versions::version_for_image(&releases, bot.image.as_deref(), &labels)
167+
}
168+
145169
#[derive(Serialize)]
146170
#[serde(rename_all = "camelCase")]
147171
struct FleetBody {
@@ -156,11 +180,10 @@ pub async fn list(State(state): State<AppState>) -> Result<Response, ApiError> {
156180
let fleet = state.fleet().await?;
157181
// Discovery already yields name order (BTreeMap), but sort here too so the
158182
// fleet page stays alphabetical even if that ever changes.
159-
let mut bots: Vec<BotBody> = fleet
160-
.bots()
161-
.iter()
162-
.map(|b| to_body(b, &state, &fleet))
163-
.collect();
183+
let mut bots = Vec::new();
184+
for b in fleet.bots() {
185+
bots.push(with_version(to_body(b, &state, &fleet), b, &state).await);
186+
}
164187
bots.sort_by(|a, b| a.name.cmp(&b.name));
165188
Ok(Json(FleetBody {
166189
bots,
@@ -175,7 +198,7 @@ pub async fn show(
175198
Path(name): Path<String>,
176199
) -> Result<Response, ApiError> {
177200
let (bot, fleet) = state.bot_and_fleet(&name).await?;
178-
Ok(Json(to_body(&bot, &state, &fleet)).into_response())
201+
Ok(Json(with_version(to_body(&bot, &state, &fleet), &bot, &state).await).into_response())
179202
}
180203

181204
/// A lifecycle action's result. Carries the bot's new state so the UI doesn't
@@ -194,7 +217,7 @@ async fn action_response(
194217
) -> Result<Response, ApiError> {
195218
let (bot, fleet) = state.bot_and_fleet(name).await?;
196219
Ok(Json(ActionBody {
197-
bot: to_body(&bot, state, &fleet),
220+
bot: with_version(to_body(&bot, state, &fleet), &bot, state).await,
198221
message,
199222
})
200223
.into_response())
@@ -1680,7 +1703,7 @@ pub async fn migrate_layout(
16801703

16811704
let (fresh, fresh_fleet) = state.bot_and_fleet(&name).await?;
16821705
Ok(Json(serde_json::json!({
1683-
"bot": to_body(&fresh, &state, &fresh_fleet),
1706+
"bot": with_version(to_body(&fresh, &state, &fresh_fleet), &fresh, &state).await,
16841707
"message": report.message(),
16851708
"movedFiles": report.moved,
16861709
"ledgersRecovered": report.ledgers_recovered,
@@ -1819,6 +1842,31 @@ mod tests {
18191842
assert_eq!(v["canStop"], false, "{body}");
18201843
}
18211844

1845+
#[tokio::test]
1846+
async fn a_bot_reports_its_release_version() {
1847+
let h = harness("bot-version");
1848+
seed_panel_bot(&h, "bot-a");
1849+
h.docker.set_container_image(
1850+
"stitch-bot-a",
1851+
"ghcr.io/textile-protocol/textile-stitch:sha-24e9192",
1852+
);
1853+
crate::panel::versions::set_test_release_index(std::collections::HashMap::from([(
1854+
"24e9192".into(),
1855+
"v0.1.226".into(),
1856+
)]));
1857+
struct ResetReleases;
1858+
impl Drop for ResetReleases {
1859+
fn drop(&mut self) {
1860+
crate::panel::versions::clear_test_release_index();
1861+
}
1862+
}
1863+
let _reset = ResetReleases;
1864+
let (status, body) = h.get("/api/bots/bot-a").await;
1865+
assert_eq!(status, StatusCode::OK, "{body}");
1866+
let v = Harness::parse(&body);
1867+
assert_eq!(v["version"], "v0.1.226", "{body}");
1868+
}
1869+
18221870
#[tokio::test]
18231871
async fn an_empty_host_lists_no_bots() {
18241872
let h = harness("empty");

0 commit comments

Comments
 (0)