Skip to content

Commit abf1d1d

Browse files
authored
feat(cli): restart command (#839)
<!-- greptile_comment --> <h2>Greptile Overview</h2> <h3>Greptile Summary</h3> This PR adds a new `restart` command to the Helix CLI that stops and starts database instances (both local Docker-based and cloud Fly.io instances). The implementation follows existing patterns from `start` and `stop` commands with proper error handling, progress tracking, and instance validation. Key changes: - **restart.rs**: New command handler with local/cloud branching logic - **docker.rs**: Added `restart_instance()` method using `docker compose restart` - **Testing**: Comprehensive lifecycle tests and CI workflow for cross-platform testing - **Data dir fix**: Changed `HELIX_DATA_DIR` to `/data` for consistent container paths The restart command properly validates instance existence, checks for build artifacts, and handles both Docker/Podman runtimes. Cloud restart uses stop+start sequence via FlyManager. Test infrastructure includes isolated temp directories via `HELIX_CACHE_DIR` override. <details><summary><h3>Important Files Changed</h3></summary> | Filename | Overview | |----------|----------| | helix-cli/src/commands/restart.rs | New restart command following established patterns. Handles local Docker restart efficiently and cloud Fly.io restart via stop+start. Minor style inconsistency with todo!/unimplemented! macros. | | helix-cli/src/docker.rs | Added restart_instance() method using docker compose restart. Changed HELIX_DATA_DIR to static /data for container consistency (intentional improvement). | | helix-cli/src/project.rs | Added HELIX_CACHE_DIR environment variable override for test isolation, properly documented and implemented. | | helix-cli/src/tests/lifecycle_tests.rs | Comprehensive lifecycle command tests covering error paths for start/stop/restart without Docker. Well-structured with clear test categories. | | .github/workflows/cli_tests.yml | New CI workflow for cross-platform CLI testing (Ubuntu/macOS/Windows). Includes cargo caching and runs both unit and integration tests. | </details> </details> <details><summary><h3>Sequence Diagram</h3></summary> ```mermaid sequenceDiagram participant User participant CLI as restart::run() participant Project as ProjectContext participant Docker as DockerManager participant Fly as FlyManager participant Container as Docker/Podman User->>CLI: helix restart [instance] CLI->>Project: find_and_load() Project-->>CLI: project context alt No instance provided & non-interactive CLI->>CLI: list_instances() CLI-->>User: Error: Available instances end CLI->>Project: get_instance(name) Project-->>CLI: instance_config alt Local Instance CLI->>Docker: new(project) CLI->>Docker: check_runtime_available() Docker-->>CLI: runtime OK CLI->>CLI: Check docker-compose.yml exists alt Compose file missing CLI-->>User: Error: Not built yet end CLI->>Docker: restart_instance(name) Docker->>Container: docker compose restart Container-->>Docker: success Docker-->>CLI: OK CLI-->>User: Container restarted else Cloud Instance (Fly.io) CLI->>CLI: Validate cluster_id CLI->>Fly: new(project, auth_type) Fly-->>CLI: fly manager CLI->>Fly: stop_instance(name) Fly->>Fly: flyctl scale count 0 Fly-->>CLI: stopped CLI->>Fly: start_instance(name) Fly->>Fly: flyctl scale count 1 Fly-->>CLI: started CLI-->>User: Cloud instance restarted end ``` </details> <!-- greptile_other_comments_section --> <!-- /greptile_comment -->
2 parents 42d5662 + 167ed72 commit abf1d1d

27 files changed

+1744
-409
lines changed

.github/workflows/cli_tests.yml

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# name: CLI Tests
2+
3+
# on:
4+
# pull_request:
5+
# branches: [main, dev]
6+
# paths:
7+
# - 'helix-cli/**'
8+
# - 'helix-macros/**'
9+
# - 'helix-container/**'
10+
# - 'helix-db/**'
11+
# - 'Cargo.toml'
12+
# - 'Cargo.lock'
13+
14+
# env:
15+
# CARGO_TERM_COLOR: always
16+
# RUST_BACKTRACE: 1
17+
18+
# jobs:
19+
# test:
20+
# name: Test (${{ matrix.os }})
21+
# runs-on: ${{ matrix.os }}
22+
# strategy:
23+
# fail-fast: false
24+
# matrix:
25+
# os: [ubuntu-latest, macos-latest, windows-latest]
26+
27+
# steps:
28+
# - name: Checkout repository
29+
# uses: actions/checkout@v4
30+
31+
# - name: Setup Rust toolchain
32+
# uses: dtolnay/rust-toolchain@stable
33+
34+
# - name: Cache cargo registry
35+
# uses: actions/cache@v4
36+
# with:
37+
# path: |
38+
# ~/.cargo/registry
39+
# ~/.cargo/git
40+
# target
41+
# key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
42+
# restore-keys: |
43+
# ${{ runner.os }}-cargo-
44+
45+
# - name: Run helix-cli unit tests
46+
# run: cargo test --release --package helix-cli
47+
# env:
48+
# # Use a unique temp directory for test isolation
49+
# HELIX_CACHE_DIR: ${{ runner.temp }}/helix-test-cache-${{ github.run_id }}
50+
51+
# - name: Run helix-macros tests
52+
# run: cargo test --release --package helix-macros
53+
54+
# # Integration tests that require repo cloning run serially
55+
# - name: Run helix-cli integration tests (ignored tests)
56+
# run: cargo test --release --package helix-cli -- --ignored --test-threads=1
57+
# env:
58+
# HELIX_CACHE_DIR: ${{ runner.temp }}/helix-test-cache-ignored-${{ github.run_id }}

.github/workflows/db_tests.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ name: Core Database Tests
33
on:
44
pull_request:
55
branches: [ main, dev ]
6+
paths:
7+
- 'helix-macros/**'
8+
- 'helix-db/**'
9+
- 'Cargo.toml'
10+
- 'Cargo.lock'
611

712
jobs:
813
test:

.github/workflows/dev_instance_tests.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@ name: Core Database Tests In Dev Instance Mode
33
on:
44
pull_request:
55
branches: [ main, dev ]
6+
paths:
7+
- 'helix-macros/**'
8+
- 'helix-db/**'
9+
- 'Cargo.toml'
10+
- 'Cargo.lock'
11+
612

713
jobs:
814
test:

.github/workflows/hql_tests.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ name: HQL Tests
33
on:
44
pull_request:
55
branches: [main, dev]
6+
paths:
7+
- 'helix-macros/**'
8+
- 'helix-db/**'
69

710
jobs:
811
hql-tests:

.github/workflows/production_db_tests.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ name: Core Database Tests In Production Mode
33
on:
44
pull_request:
55
branches: [ main, dev ]
6+
paths:
7+
- 'helix-macros/**'
8+
- 'helix-db/**'
9+
- 'Cargo.toml'
10+
- 'Cargo.lock'
611

712
jobs:
813
test:

Cargo.lock

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

helix-cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ crossterm = "0.28"
3636

3737
[dev-dependencies]
3838
tempfile = "3.23.0"
39+
serial_test = "3.2"
3940

4041
[lib]
4142
name = "helix_cli"

helix-cli/build.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ export PATH="$HOME/.cargo/bin:$PATH"
77

88
# if dev profile, build with dev profile
99
if [ "$1" = "dev" ]; then
10-
cargo install --profile dev --force --path . --root ~/.local
10+
cargo install --debug --force --path . --root ~/.local
1111
else
12-
cargo install --release --force --path . --root ~/.local
12+
cargo install --force --path . --root ~/.local
1313
fi
1414

1515
if ! echo "$PATH" | grep -q "$HOME/.local/bin"; then

helix-cli/src/commands/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ pub mod migrate;
1616
pub mod prune;
1717
pub mod pull;
1818
pub mod push;
19+
pub mod restart;
1920
pub mod start;
2021
pub mod status;
2122
pub mod stop;

helix-cli/src/commands/restart.rs

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
use crate::commands::integrations::fly::FlyManager;
2+
use crate::config::CloudConfig;
3+
use crate::docker::DockerManager;
4+
use crate::output::{Operation, Step};
5+
use crate::project::ProjectContext;
6+
use crate::prompts;
7+
use eyre::{OptionExt, Result};
8+
9+
pub async fn run(instance_name: Option<String>) -> Result<()> {
10+
// Load project context
11+
let project = ProjectContext::find_and_load(None)?;
12+
13+
// Get instance name - prompt if not provided
14+
let instance_name = match instance_name {
15+
Some(name) => name,
16+
None if prompts::is_interactive() => {
17+
let instances = project.config.list_instances_with_types();
18+
prompts::select_instance(&instances)?
19+
}
20+
None => {
21+
let instances = project.config.list_instances();
22+
return Err(eyre::eyre!(
23+
"No instance specified. Available instances: {}",
24+
instances
25+
.into_iter()
26+
.cloned()
27+
.collect::<Vec<_>>()
28+
.join(", ")
29+
));
30+
}
31+
};
32+
33+
// Get instance config
34+
let instance_config = project.config.get_instance(&instance_name)?;
35+
36+
if instance_config.is_local() {
37+
restart_local_instance(&project, &instance_name).await
38+
} else {
39+
restart_cloud_instance(&project, &instance_name, instance_config.into()).await
40+
}
41+
}
42+
43+
async fn restart_local_instance(project: &ProjectContext, instance_name: &str) -> Result<()> {
44+
let op = Operation::new("Restarting", instance_name);
45+
46+
let docker = DockerManager::new(project);
47+
48+
// Check Docker availability
49+
DockerManager::check_runtime_available(docker.runtime)?;
50+
51+
// Check if instance is built (has docker-compose.yml)
52+
let workspace = project.instance_workspace(instance_name);
53+
let compose_file = workspace.join("docker-compose.yml");
54+
55+
if !compose_file.exists() {
56+
op.failure();
57+
let error = crate::errors::CliError::new(format!(
58+
"instance '{instance_name}' has not been built yet"
59+
))
60+
.with_hint(format!(
61+
"run 'helix build {instance_name}' first to build the instance"
62+
));
63+
return Err(eyre::eyre!("{}", error.render()));
64+
}
65+
66+
// Restart the instance
67+
let mut restart_step = Step::with_messages("Restarting container", "Container restarted");
68+
restart_step.start();
69+
docker.restart_instance(instance_name)?;
70+
restart_step.done();
71+
72+
op.success();
73+
74+
Ok(())
75+
}
76+
77+
async fn restart_cloud_instance(
78+
project: &ProjectContext,
79+
instance_name: &str,
80+
instance_config: CloudConfig,
81+
) -> Result<()> {
82+
let op = Operation::new("Restarting", instance_name);
83+
84+
let _cluster_id = instance_config.get_cluster_id().ok_or_eyre(format!(
85+
"Cloud instance '{instance_name}' must have a cluster_id"
86+
))?;
87+
88+
let mut restart_step =
89+
Step::with_messages("Restarting cloud instance", "Cloud instance restarted");
90+
restart_step.start();
91+
92+
match instance_config {
93+
CloudConfig::FlyIo(config) => {
94+
Step::verbose_substep("Stopping Fly.io instance...");
95+
let fly = FlyManager::new(project, config.auth_type.clone()).await?;
96+
fly.stop_instance(instance_name).await?;
97+
98+
Step::verbose_substep("Starting Fly.io instance...");
99+
fly.start_instance(instance_name).await?;
100+
}
101+
CloudConfig::Helix(_config) => {
102+
todo!()
103+
}
104+
CloudConfig::Ecr(_config) => {
105+
unimplemented!()
106+
}
107+
}
108+
109+
restart_step.done();
110+
op.success();
111+
112+
Ok(())
113+
}

0 commit comments

Comments
 (0)