Skip to content

Commit a05e3e9

Browse files
authored
feat(cli): implement upgrade command (#40)
* feat(cli): implement upgrade command - Add upgrade command to check for and download latest releases from GitHub - Compare current version with latest release, skip if already up to date - Download new binaries for all configured platforms - Update init command to download real binaries from GitHub releases - Add git repo root check to init (errors if not at root) - Add serde_json dependency for GitHub API parsing - Add rustls-tls and json features to reqwest Closes #11 * test: initialize git repo in integration tests Init command now requires being at a git repo root, so the integration tests need to run git init in temp directories before testing the init command. * feat(cli): add --force flag to skip git repo root check Adds --force flag to init command that allows initialization in directories that aren't git repository roots. Useful for edge cases or non-git projects. Also adds integration test to verify the flag works correctly. * test: build without network feature for integration tests No releases exist yet, so init cannot download real binaries. Building without network feature uses placeholder binaries instead.
1 parent 7f65d74 commit a05e3e9

File tree

5 files changed

+266
-40
lines changed

5 files changed

+266
-40
lines changed

.github/workflows/integration-test.yml

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ jobs:
6262
${{ runner.os }}-cargo-
6363
6464
- name: Build release binary
65-
run: cargo build --release
65+
# Build without network feature so init uses placeholder binaries (no releases exist yet)
66+
run: cargo build --release --no-default-features --features parallel
6667

6768
- name: Copy binary to test location
6869
shell: bash
@@ -232,6 +233,7 @@ jobs:
232233
run: |
233234
INIT_DIR=$(mktemp -d)
234235
cd "$INIT_DIR"
236+
git init
235237
$GITHUB_WORKSPACE/tests/${{ matrix.binary }} init --current-platform-only
236238
237239
# Verify files were created
@@ -271,6 +273,7 @@ jobs:
271273
run: |
272274
INIT_DIR=$(mktemp -d)
273275
cd "$INIT_DIR"
276+
git init
274277
$GITHUB_WORKSPACE/tests/${{ matrix.binary }} init --platforms linux-amd64,macos-arm64,windows-amd64
275278
276279
# Verify config has the right platforms
@@ -309,6 +312,7 @@ jobs:
309312
run: |
310313
INIT_DIR=$(mktemp -d)
311314
cd "$INIT_DIR"
315+
git init
312316
313317
# First init with one platform
314318
$GITHUB_WORKSPACE/tests/${{ matrix.binary }} init --platforms linux-amd64
@@ -345,6 +349,7 @@ jobs:
345349
run: |
346350
INIT_DIR=$(mktemp -d)
347351
cd "$INIT_DIR"
352+
git init
348353
$GITHUB_WORKSPACE/tests/${{ matrix.binary }} init --platforms linux-amd64,windows-amd64
349354
350355
OUTPUT=$($GITHUB_WORKSPACE/tests/${{ matrix.binary }} init --show-platforms 2>&1)
@@ -365,6 +370,7 @@ jobs:
365370
run: |
366371
INIT_DIR=$(mktemp -d)
367372
cd "$INIT_DIR"
373+
git init
368374
$GITHUB_WORKSPACE/tests/${{ matrix.binary }} init --platforms linux-amd64
369375
370376
# Try to remove the only platform - should fail
@@ -376,6 +382,31 @@ jobs:
376382
echo "✅ init correctly refuses to remove last platform"
377383
rm -rf "$INIT_DIR"
378384
385+
- name: "Test: init --force skips git repo check"
386+
shell: bash
387+
run: |
388+
INIT_DIR=$(mktemp -d)
389+
cd "$INIT_DIR"
390+
# Do NOT run git init - this is intentionally not a git repo
391+
392+
# Without --force, should fail
393+
if $GITHUB_WORKSPACE/tests/${{ matrix.binary }} init --current-platform-only 2>&1; then
394+
echo "ERROR: init should fail without --force in non-git directory"
395+
exit 1
396+
fi
397+
398+
# With --force, should succeed
399+
$GITHUB_WORKSPACE/tests/${{ matrix.binary }} init --current-platform-only --force
400+
401+
# Verify files were created
402+
if [ ! -f ".rnr/config.yaml" ]; then
403+
echo "ERROR: .rnr/config.yaml not created with --force"
404+
exit 1
405+
fi
406+
407+
echo "✅ init --force correctly skips git repo check"
408+
rm -rf "$INIT_DIR"
409+
379410
# ==================== Error Cases ====================
380411

381412
- name: "Test: nonexistent task (should fail)"

Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@ dialoguer = "0.11"
2828
console = "0.15"
2929

3030
# HTTP client for init/upgrade
31-
reqwest = { version = "0.12", features = ["blocking"], default-features = false, optional = true }
31+
reqwest = { version = "0.12", features = ["blocking", "rustls-tls", "json"], default-features = false, optional = true }
32+
33+
# JSON parsing for GitHub API
34+
serde_json = "1"
3235

3336
# Async runtime for parallel execution
3437
tokio = { version = "1", features = ["rt-multi-thread", "process", "sync"], optional = true }

src/cli.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,8 @@ pub struct InitArgs {
5151
/// Show currently configured platforms
5252
#[arg(long)]
5353
pub show_platforms: bool,
54+
55+
/// Skip git repository root check
56+
#[arg(long)]
57+
pub force: bool,
5458
}

src/commands/init.rs

Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,15 @@ pub fn run(args: &InitArgs) -> Result<()> {
3838
return Ok(());
3939
}
4040

41+
// Error if not at git repo root (unless --force is used)
42+
if !args.force && !is_git_repo_root()? {
43+
bail!(
44+
"This directory does not appear to be a git repository root.\n\
45+
rnr is typically initialized at the root of a git repository.\n\
46+
Use --force to initialize anyway, or run from the directory containing your .git folder."
47+
);
48+
}
49+
4150
// Determine platforms to install
4251
let platforms = select_platforms(args)?;
4352

@@ -49,6 +58,13 @@ pub fn run(args: &InitArgs) -> Result<()> {
4958
initialize(&platforms)
5059
}
5160

61+
/// Check if the current directory is a git repository root
62+
fn is_git_repo_root() -> Result<bool> {
63+
let current_dir = std::env::current_dir().context("Failed to get current directory")?;
64+
let git_dir = current_dir.join(".git");
65+
Ok(git_dir.exists())
66+
}
67+
5268
/// Select platforms based on args or interactively
5369
fn select_platforms(args: &InitArgs) -> Result<Vec<Platform>> {
5470
// --all-platforms
@@ -191,28 +207,42 @@ fn download_binaries(platforms: &[Platform], bin_directory: &Path) -> Result<()>
191207
Ok(())
192208
}
193209

210+
/// GitHub repository for releases
211+
const GITHUB_REPO: &str = "CodingWithCalvin/rnr.cli";
212+
194213
/// Download a single binary from GitHub releases
195214
#[cfg(feature = "network")]
196215
fn download_binary(platform: Platform, dest: &Path) -> Result<()> {
197-
use std::io::Write;
198-
199-
// TODO: Replace with actual release URL once we have releases
200-
// For now, use GitHub releases URL pattern
201216
let url = format!(
202-
"https://github.com/CodingWithCalvin/rnr.cli/releases/latest/download/{}",
217+
"https://github.com/{}/releases/latest/download/{}",
218+
GITHUB_REPO,
203219
platform.binary_name()
204220
);
205221

206-
// For now, create a placeholder since we don't have releases yet
207-
// In production, this would download from the URL
208-
let placeholder = format!(
209-
"#!/bin/sh\necho 'Placeholder binary for {}. Replace with actual binary from releases.'\n",
210-
platform.id()
211-
);
222+
let client = reqwest::blocking::Client::builder()
223+
.user_agent("rnr-cli")
224+
.build()
225+
.context("Failed to create HTTP client")?;
212226

213-
let mut file =
214-
fs::File::create(dest).with_context(|| format!("Failed to create {}", dest.display()))?;
215-
file.write_all(placeholder.as_bytes())?;
227+
let response = client
228+
.get(&url)
229+
.send()
230+
.with_context(|| format!("Failed to download {}", platform.binary_name()))?;
231+
232+
if !response.status().is_success() {
233+
anyhow::bail!(
234+
"Failed to download {}: HTTP {}",
235+
platform.binary_name(),
236+
response.status().as_u16()
237+
);
238+
}
239+
240+
let bytes = response
241+
.bytes()
242+
.with_context(|| format!("Failed to read response for {}", platform.binary_name()))?;
243+
244+
// Write to file
245+
fs::write(dest, &bytes).with_context(|| format!("Failed to write {}", dest.display()))?;
216246

217247
// Make executable on Unix
218248
#[cfg(unix)]
@@ -223,14 +253,6 @@ fn download_binary(platform: Platform, dest: &Path) -> Result<()> {
223253
fs::set_permissions(dest, perms)?;
224254
}
225255

226-
// TODO: Actual download implementation:
227-
// let response = reqwest::blocking::get(&url)
228-
// .with_context(|| format!("Failed to download {}", url))?;
229-
// let bytes = response.bytes()?;
230-
// fs::write(dest, bytes)?;
231-
232-
let _ = url; // Suppress unused warning for now
233-
234256
Ok(())
235257
}
236258

0 commit comments

Comments
 (0)