Skip to content

Commit 5d4c386

Browse files
committed
fix(panel): allow Update for bots on sha-* image pins
- Treat bots running same-repo `sha-*` or digest pins as eligible to Update onto the configured channel's target, enabling bots pinned at creation time to leave the pin via Update - Clarify that only the *bot's* image needs to be a pin to be eligible; when `STITCH_PANEL_BOT_IMAGE` itself is a pin, bots on the resolved `:latest` target remain eligible for later releases - Update error message to reflect that Update works for same-repo sha-* / digest pins moving to the resolved target - Add tests verifying sha-pinned bots can Update when panel targets `:latest`, and bots on old pins can Update when panel advances to new pins
1 parent 97b19ec commit 5d4c386

6 files changed

Lines changed: 90 additions & 24 deletions

File tree

.textile-monorepo-source

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
24d3dc31eb7189aa778aebb81a5174c8f58e0bc2
1+
91134999768b376b01ab7022be8a43ba029dad41

.textile-stitch-release-version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.1.120
1+
0.1.121

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.120"
3+
version = "0.1.121"
44
edition = "2021"
55
description = "Stitch — Textile filler-network operator bot; market-makes the filler order book with signed UniswapX limit orders."
66
license = "AGPL-3.0-or-later"

src/panel/http/bots.rs

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -872,8 +872,9 @@ async fn recreate_on_image(
872872
) {
873873
return Err(ApiError::conflict(format!(
874874
"{name} runs {current}, which is not on the update channel for {}. Update \
875-
only refreshes bots already on STITCH_PANEL_BOT_IMAGE (sha-* pins may \
876-
move to :latest) — recreate it from your own compose file, or change \
875+
only refreshes bots on STITCH_PANEL_BOT_IMAGE's repository and tag \
876+
channel (same-repo sha-* / digest pins may move to the resolved \
877+
target) — recreate it from your own compose file, or change \
877878
STITCH_PANEL_BOT_IMAGE.",
878879
state.cfg.bot_image
879880
)));
@@ -2866,6 +2867,65 @@ mod tests {
28662867
);
28672868
}
28682869

2870+
#[tokio::test]
2871+
async fn update_allows_a_sha_pinned_bot_when_the_panel_targets_latest() {
2872+
// Production often pins bots at create time while STITCH_PANEL_BOT_IMAGE
2873+
// is `:latest` (or later moves there). Those bots must still be able to
2874+
// leave the pin via Update — not get a channel-gate conflict.
2875+
let h = super::super::testkit::harness_with_bot_image(
2876+
"bot-update-sha-on-latest",
2877+
"ghcr.io/textile-protocol/textile-stitch:latest",
2878+
);
2879+
seed_panel_bot(&h, "bot-a");
2880+
h.docker.set_container_image(
2881+
"stitch-bot-a",
2882+
"ghcr.io/textile-protocol/textile-stitch:sha-oldc0de",
2883+
);
2884+
2885+
let (status, body) = h
2886+
.post_json("/api/bots/bot-a/update", serde_json::json!({}))
2887+
.await;
2888+
assert_eq!(status, StatusCode::OK, "{body}");
2889+
let expected = "ghcr.io/textile-protocol/textile-stitch:latest";
2890+
assert!(
2891+
h.docker.calls().iter().any(|c| matches!(
2892+
c,
2893+
Call::EnsureImage { image, refresh: true } if image == expected
2894+
)),
2895+
"sha-pinned bot must Update onto the panel's :latest channel, got {:?}",
2896+
h.docker.calls()
2897+
);
2898+
}
2899+
2900+
#[tokio::test]
2901+
async fn update_allows_a_bot_on_a_different_sha_pin_than_the_panel() {
2902+
// Panel env advanced to a new sha-* pin; existing bots still run the old
2903+
// one. Update resolves pins to :latest, so they must not be stuck.
2904+
let h = super::super::testkit::harness_with_bot_image(
2905+
"bot-update-other-sha",
2906+
"ghcr.io/textile-protocol/textile-stitch:sha-newpin00",
2907+
);
2908+
seed_panel_bot(&h, "bot-a");
2909+
h.docker.set_container_image(
2910+
"stitch-bot-a",
2911+
"ghcr.io/textile-protocol/textile-stitch:sha-oldpin00",
2912+
);
2913+
2914+
let (status, body) = h
2915+
.post_json("/api/bots/bot-a/update", serde_json::json!({}))
2916+
.await;
2917+
assert_eq!(status, StatusCode::OK, "{body}");
2918+
assert!(
2919+
h.docker.calls().iter().any(|c| matches!(
2920+
c,
2921+
Call::EnsureImage { image, refresh: true }
2922+
if image == "ghcr.io/textile-protocol/textile-stitch:latest"
2923+
)),
2924+
"old sha pin must still Update to :latest, got {:?}",
2925+
h.docker.calls()
2926+
);
2927+
}
2928+
28692929
#[tokio::test]
28702930
async fn update_refuses_when_the_fresh_pull_fails() {
28712931
// Same contract as panel self-update: a pull failure must not destroy the

src/panel/updates.rs

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -91,34 +91,34 @@ pub fn same_image_repository(a: &str, b: &str) -> bool {
9191
/// Whether a running bot image is on `STITCH_PANEL_BOT_IMAGE`'s update channel.
9292
///
9393
/// Same repository is not enough: a bot on `:canary` must not be Updated onto
94-
/// `:latest` just because they share a repo. Mutable tags must match. An
95-
/// explicit `sha-*` / digest pin may Update onto the resolved `:latest` target,
96-
/// and bots already on that resolved target stay eligible for later releases
94+
/// `:latest` just because they share a repo. Mutable tags must match.
95+
///
96+
/// An explicit `sha-*` / digest pin on the *bot* may always Update onto the
97+
/// configured channel's target — operators pin for reproducibility, then use
98+
/// Update to leave the pin. When the *configured* image is itself a pin, bots
99+
/// already on the resolved `:latest` target stay eligible for later releases
97100
/// (otherwise a successful pin→latest Update would leave them "off channel"
98101
/// forever while the env still names the pin).
99102
pub fn bot_eligible_for_configured_update(current: &str, configured: &str) -> bool {
100103
if !same_image_repository(current, configured) {
101104
return false;
102105
}
103106
let cur = parse_image_ref(current);
107+
// Any same-repo sha-* / digest pin can leave the pin via Update, whether
108+
// STITCH_PANEL_BOT_IMAGE is `:latest`, another pin, or a mutable channel.
109+
if cur.tag.starts_with("sha-") || current.contains('@') {
110+
return true;
111+
}
104112
let cfg = parse_image_ref(configured);
105113
let cfg_is_pin = cfg.tag.starts_with("sha-") || configured.contains('@');
106114
if cfg_is_pin {
107-
let on_pin = if configured.contains('@') {
108-
current == configured
109-
} else {
110-
cur.tag == cfg.tag
111-
};
112-
if on_pin {
113-
return true;
114-
}
115115
// Already moved to the resolved update target (:latest). Exact ref only —
116-
// digest refs parse with a synthetic `latest` tag, so tag equality would
117-
// treat any same-repo `@sha256:…` as on-channel.
116+
// digest refs are handled above as pins (a synthetic `latest` tag would
117+
// otherwise treat any same-repo `@sha256:…` as on-channel).
118118
return update_target_image(configured).is_some_and(|target| current == target);
119119
}
120-
// Mutable channel: already on that tag (not a digest-only ref of another build).
121-
!current.contains('@') && cur.tag == cfg.tag
120+
// Mutable channel: already on that tag.
121+
cur.tag == cfg.tag
122122
}
123123

124124
/// True when `remote` (a `sha256:…` digest) is not among the local RepoDigests.
@@ -481,6 +481,7 @@ mod tests {
481481
let latest = "ghcr.io/textile-protocol/textile-stitch:latest";
482482
let canary = "ghcr.io/textile-protocol/textile-stitch:canary";
483483
let pin = "ghcr.io/textile-protocol/textile-stitch:sha-deadbeef";
484+
let other_pin = "ghcr.io/textile-protocol/textile-stitch:sha-cafebabe";
484485
let digest_pin = "ghcr.io/textile-protocol/textile-stitch@sha256:aaaa";
485486
assert!(bot_eligible_for_configured_update(latest, latest));
486487
assert!(!bot_eligible_for_configured_update(canary, latest));
@@ -492,14 +493,19 @@ mod tests {
492493
"ghcr.io/acme/stitch-fork:v9",
493494
latest
494495
));
495-
// A different digest must not look "already on :latest" via synthetic tags.
496-
assert!(!bot_eligible_for_configured_update(
496+
// Bots still on a sha-* / digest pin must be able to Update onto the
497+
// panel's mutable channel (the common "panel is :latest, bot is sha-…"
498+
// case) — and onto :latest when the env itself still names a pin.
499+
assert!(bot_eligible_for_configured_update(pin, latest));
500+
assert!(bot_eligible_for_configured_update(digest_pin, latest));
501+
assert!(bot_eligible_for_configured_update(other_pin, pin));
502+
assert!(bot_eligible_for_configured_update(
497503
"ghcr.io/textile-protocol/textile-stitch@sha256:other",
498504
pin
499505
));
500506
assert!(bot_eligible_for_configured_update(digest_pin, digest_pin));
501507
assert!(bot_eligible_for_configured_update(latest, digest_pin));
502-
assert!(!bot_eligible_for_configured_update(
508+
assert!(bot_eligible_for_configured_update(
503509
"ghcr.io/textile-protocol/textile-stitch@sha256:bbbb",
504510
digest_pin
505511
));

0 commit comments

Comments
 (0)