Skip to content

Commit a33a47e

Browse files
committed
chore: add mock health plugin
1 parent 2e268a4 commit a33a47e

3 files changed

Lines changed: 246 additions & 0 deletions

File tree

plugins/repos-health/Cargo.toml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[package]
2+
name = "repos-health"
3+
version = "0.1.0"
4+
edition = "2024"
5+
6+
[[bin]]
7+
name = "repos-health"
8+
path = "src/main.rs"
9+
10+
[dependencies]
11+
anyhow = "1"
12+
serde = { version = "1", features = ["derive"] }
13+
serde_json = "1"
14+
chrono = { version = "0.4", features = ["serde"] }
15+
16+
[dependencies.repos]
17+
path = "../.."

plugins/repos-health/README.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# repos-health
2+
3+
A health check plugin for the repos tool that:
4+
5+
- Scans each repository for `package.json` files
6+
- Checks for outdated npm dependencies using `npm outdated`
7+
- Updates dependencies using `npm update`
8+
- Creates git branches and commits changes
9+
- Opens pull requests for dependency updates
10+
11+
## Requirements
12+
13+
- Node.js and npm installed
14+
- Git repository with push permissions
15+
- GitHub token configured for PR creation
16+
17+
## Usage
18+
19+
```bash
20+
repos health
21+
```
22+
23+
The plugin will:
24+
25+
1. Load the default repos configuration
26+
2. Process each repository that contains a `package.json`
27+
3. Check for outdated dependencies
28+
4. Update dependencies if found
29+
5. Create a branch and commit changes
30+
6. Push the branch and open a PR
31+
32+
## Output
33+
34+
The plugin reports:
35+
36+
- Repositories processed
37+
- Outdated packages found
38+
- Successful dependency updates
39+
- PR creation status

plugins/repos-health/src/main.rs

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
use anyhow::{Context, Result};
2+
use chrono::Utc;
3+
use repos::{Repository, load_default_config};
4+
use std::env;
5+
use std::path::Path;
6+
use std::process::{Command, Stdio};
7+
8+
fn main() -> Result<()> {
9+
let args: Vec<String> = env::args().collect();
10+
11+
// Handle --help
12+
if args.len() > 1 && (args[1] == "--help" || args[1] == "-h") {
13+
print_help();
14+
return Ok(());
15+
}
16+
17+
let config = load_default_config().context("load repos config")?;
18+
let repos = config.repositories;
19+
let mut processed = 0;
20+
for repo in repos {
21+
if let Err(e) = process_repo(&repo) {
22+
eprintln!("health: {} skipped: {}", repo.name, e);
23+
} else {
24+
processed += 1;
25+
}
26+
}
27+
println!("health: processed {} repositories", processed);
28+
Ok(())
29+
}
30+
31+
fn print_help() {
32+
println!("repos-health - Check and update npm dependencies in repositories");
33+
println!();
34+
println!("USAGE:");
35+
println!(" repos health [OPTIONS]");
36+
println!();
37+
println!("DESCRIPTION:");
38+
println!(" Scans repositories for outdated npm packages and automatically");
39+
println!(" updates them, creates branches, and commits changes.");
40+
println!();
41+
println!(" For each repository with a package.json file:");
42+
println!(" 1. Checks for outdated npm packages");
43+
println!(" 2. Updates packages if found");
44+
println!(" 3. Creates a branch and commits changes");
45+
println!(" 4. Pushes the branch to origin");
46+
println!();
47+
println!(" To create pull requests for the updated branches, use:");
48+
println!(" repos pr --title 'chore: dependency updates' <repo-names>");
49+
println!();
50+
println!("REQUIREMENTS:");
51+
println!(" - npm must be installed and available in PATH");
52+
println!(" - Repositories must have package.json files");
53+
println!(" - Git repositories must be properly initialized");
54+
println!();
55+
println!("OPTIONS:");
56+
println!(" -h, --help Print this help message");
57+
}
58+
59+
fn process_repo(repo: &Repository) -> Result<()> {
60+
let repo_path = repo.get_target_dir();
61+
let path = Path::new(&repo_path);
62+
let pkg = path.join("package.json");
63+
if !pkg.exists() {
64+
anyhow::bail!("no package.json");
65+
}
66+
67+
let outdated = check_outdated(path)?;
68+
if outdated.is_empty() {
69+
println!("health: {} up-to-date", repo.name);
70+
return Ok(());
71+
}
72+
73+
println!(
74+
"health: {} outdated packages: {}",
75+
repo.name,
76+
outdated.join(", ")
77+
);
78+
update_dependencies(path)?;
79+
let changed = has_lockfile_changes(path)?;
80+
if !changed {
81+
println!("health: {} no lockfile changes after update", repo.name);
82+
return Ok(());
83+
}
84+
85+
let branch = format!("health/deps-{}", short_timestamp());
86+
create_branch_and_commit(path, &branch, repo, &outdated)?;
87+
push_branch(path, &branch)?;
88+
println!(
89+
"health: {} branch {} pushed - use 'repos pr' to create pull request",
90+
repo.name, branch
91+
);
92+
Ok(())
93+
}
94+
95+
fn check_outdated(repo_path: &Path) -> Result<Vec<String>> {
96+
// Try npm outdated --json; if npm missing or error, return mock info
97+
let output = Command::new("npm")
98+
.arg("outdated")
99+
.arg("--json")
100+
.current_dir(repo_path)
101+
.stdout(Stdio::piped())
102+
.stderr(Stdio::null())
103+
.output();
104+
105+
match output {
106+
Ok(o) if o.status.success() || o.status.code() == Some(1) => {
107+
// npm outdated exits 1 if there are outdated deps
108+
if o.stdout.is_empty() {
109+
return Ok(vec![]);
110+
}
111+
let v: serde_json::Value =
112+
serde_json::from_slice(&o.stdout).context("parse npm outdated json")?;
113+
let mut deps = Vec::new();
114+
if let serde_json::Value::Object(map) = v {
115+
for (name, info) in map {
116+
if info.get("latest").is_some() {
117+
deps.push(name);
118+
}
119+
}
120+
}
121+
Ok(deps)
122+
}
123+
Ok(_) => Ok(vec![]),
124+
Err(_) => {
125+
// Mock fallback when npm not present
126+
Ok(vec![]) // keep empty for minimal intrusive behavior
127+
}
128+
}
129+
}
130+
131+
fn update_dependencies(repo_path: &Path) -> Result<()> {
132+
// Best effort upgrade; ignore failures to keep minimal
133+
let _ = Command::new("npm")
134+
.arg("update")
135+
.current_dir(repo_path)
136+
.status();
137+
Ok(())
138+
}
139+
140+
fn has_lockfile_changes(repo_path: &Path) -> Result<bool> {
141+
// Check git diff for package-lock.json / yarn.lock / pnpm-lock.yaml
142+
let patterns = ["package-lock.json", "yarn.lock", "pnpm-lock.yaml"];
143+
let output = Command::new("git")
144+
.arg("status")
145+
.arg("--porcelain")
146+
.current_dir(repo_path)
147+
.output()
148+
.context("git status")?;
149+
let text = String::from_utf8_lossy(&output.stdout);
150+
Ok(patterns.iter().any(|p| text.contains(p)))
151+
}
152+
153+
fn create_branch_and_commit(
154+
repo_path: &Path,
155+
branch: &str,
156+
repo: &Repository,
157+
deps: &[String],
158+
) -> Result<()> {
159+
run(repo_path, ["git", "checkout", "-b", branch])?;
160+
run(repo_path, ["git", "add", "."])?; // minimal; could restrict
161+
let msg = format!("chore(health): update dependencies ({})", deps.join(", "));
162+
run(repo_path, ["git", "commit", "-m", &msg])?;
163+
println!(
164+
"health: {} committed dependency updates on {}",
165+
repo.name, branch
166+
);
167+
Ok(())
168+
}
169+
170+
fn push_branch(repo_path: &Path, branch: &str) -> Result<()> {
171+
run(repo_path, ["git", "push", "-u", "origin", branch])?;
172+
Ok(())
173+
}
174+
175+
fn run<P: AsRef<Path>, const N: usize>(cwd: P, cmd: [&str; N]) -> Result<()> {
176+
let status = Command::new(cmd[0])
177+
.args(&cmd[1..])
178+
.current_dir(cwd.as_ref())
179+
.status()
180+
.with_context(|| format!("exec {:?}", cmd))?;
181+
if !status.success() {
182+
anyhow::bail!("command {:?} failed", cmd);
183+
}
184+
Ok(())
185+
}
186+
187+
fn short_timestamp() -> String {
188+
let now = Utc::now();
189+
format!("{}", now.format("%Y%m%d"))
190+
}

0 commit comments

Comments
 (0)