Skip to content

Commit bed52f6

Browse files
authored
feat: runner rotation detection with polling and CI/headless mode (#3)
- Add TOML config (config.rs) with poll_interval_secs, poll_timeout_secs, gitlab_host, gitlab_token - Extend RunnerManager with platform and architecture optional fields - Add detect_rotating_runners() to Conductor (runners with >1 manager) - Add Rotate variant to Command enum, wire into execute_search and UI - Add rotation table renderer with old/new manager comparison - Add polling toggle (p key) with timer in header and auto-refresh - Add --watch, --command, --tags CLI flags for headless/CI mode - Bump version to 0.3.0 - 51 tests passing, clippy clean, fmt clean
1 parent c04db1a commit bed52f6

9 files changed

Lines changed: 795 additions & 13 deletions

File tree

Cargo.lock

Lines changed: 132 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "gitlab-runner-tui"
3-
version = "0.1.0"
3+
version = "0.3.0"
44
edition = "2021"
55

66
[dependencies]
@@ -27,6 +27,8 @@ anyhow = "1.0"
2727
# CLI & Config
2828
clap = { version = "4.5", features = ["derive", "env"] }
2929
dotenvy = "0.15"
30+
toml = "0.8"
31+
dirs = "5.0"
3032

3133
# Utility
3234
chrono = { version = "0.4", features = ["serde"] }

src/conductor/mod.rs

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,15 @@ impl Conductor {
133133
.collect();
134134
Ok(empty)
135135
}
136+
137+
pub async fn detect_rotating_runners(&self, filters: RunnerFilters) -> Result<Vec<Runner>> {
138+
let runners = self.fetch_runners(filters).await?;
139+
let rotating = runners
140+
.into_iter()
141+
.filter(|r| r.managers.len() > 1)
142+
.collect();
143+
Ok(rotating)
144+
}
136145
}
137146

138147
#[cfg(test)]
@@ -430,4 +439,125 @@ mod tests {
430439
mock.assert_async().await;
431440
}
432441
}
442+
443+
#[tokio::test]
444+
async fn test_detect_rotating_runners_finds_multi_manager() {
445+
let mut server = Server::new_async().await;
446+
let mocks = setup_runner_mocks(
447+
&mut server,
448+
&[
449+
// Runner 1: two managers (rotation in progress)
450+
(1, "online", &["prod"], &[(10, "online"), (11, "offline")]),
451+
// Runner 2: single manager (no rotation)
452+
(2, "online", &["staging"], &[(20, "online")]),
453+
],
454+
)
455+
.await;
456+
457+
let client = GitLabClient::new(server.url(), "test-token".to_string()).unwrap();
458+
let conductor = Conductor::new(client);
459+
460+
let rotating = conductor
461+
.detect_rotating_runners(RunnerFilters::default())
462+
.await
463+
.unwrap();
464+
465+
assert_eq!(rotating.len(), 1);
466+
assert_eq!(rotating[0].id, 1);
467+
assert_eq!(rotating[0].managers.len(), 2);
468+
469+
for mock in &mocks {
470+
mock.assert_async().await;
471+
}
472+
}
473+
474+
#[tokio::test]
475+
async fn test_detect_rotating_runners_empty_when_no_rotation() {
476+
let mut server = Server::new_async().await;
477+
let mocks = setup_runner_mocks(
478+
&mut server,
479+
&[
480+
(1, "online", &["prod"], &[(10, "online")]),
481+
(2, "online", &["staging"], &[(20, "online")]),
482+
],
483+
)
484+
.await;
485+
486+
let client = GitLabClient::new(server.url(), "test-token".to_string()).unwrap();
487+
let conductor = Conductor::new(client);
488+
489+
let rotating = conductor
490+
.detect_rotating_runners(RunnerFilters::default())
491+
.await
492+
.unwrap();
493+
494+
assert!(rotating.is_empty());
495+
496+
for mock in &mocks {
497+
mock.assert_async().await;
498+
}
499+
}
500+
501+
#[tokio::test]
502+
async fn test_detect_rotating_runners_excludes_no_managers() {
503+
let mut server = Server::new_async().await;
504+
let mocks = setup_runner_mocks(
505+
&mut server,
506+
&[
507+
(1, "online", &["prod"], &[]),
508+
(
509+
2,
510+
"online",
511+
&["staging"],
512+
&[(20, "offline"), (21, "online")],
513+
),
514+
],
515+
)
516+
.await;
517+
518+
let client = GitLabClient::new(server.url(), "test-token".to_string()).unwrap();
519+
let conductor = Conductor::new(client);
520+
521+
let rotating = conductor
522+
.detect_rotating_runners(RunnerFilters::default())
523+
.await
524+
.unwrap();
525+
526+
assert_eq!(rotating.len(), 1);
527+
assert_eq!(rotating[0].id, 2);
528+
529+
for mock in &mocks {
530+
mock.assert_async().await;
531+
}
532+
}
533+
534+
#[tokio::test]
535+
async fn test_detect_rotating_runners_three_managers() {
536+
let mut server = Server::new_async().await;
537+
let mocks = setup_runner_mocks(
538+
&mut server,
539+
&[(
540+
1,
541+
"online",
542+
&["prod"],
543+
&[(10, "offline"), (11, "stale"), (12, "online")],
544+
)],
545+
)
546+
.await;
547+
548+
let client = GitLabClient::new(server.url(), "test-token".to_string()).unwrap();
549+
let conductor = Conductor::new(client);
550+
551+
let rotating = conductor
552+
.detect_rotating_runners(RunnerFilters::default())
553+
.await
554+
.unwrap();
555+
556+
assert_eq!(rotating.len(), 1);
557+
assert_eq!(rotating[0].managers.len(), 3);
558+
559+
for mock in &mocks {
560+
mock.assert_async().await;
561+
}
562+
}
433563
}

0 commit comments

Comments
 (0)