feat(czdev): add bump command + install docs in README

czdev bump: queries published versions, shows next patch version.
README: added install instructions and publish workflow.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
LiHaohua
2026-05-09 15:14:29 +08:00
co-authored by Claude Opus 4.6
parent ca6d6fbfc4
commit 34e3f771cd
3 changed files with 186 additions and 0 deletions
+48
View File
@@ -145,6 +145,54 @@ cargo run -p czdev --release -- deploy \
--deb path/to/my_app_arm64.deb
```
### 8. Publishing to the AppStore
```bash
# Install czdev (one-time, or build from source: cargo build --release -p czdev)
curl -fsSL https://github.com/m5stack/CardputerZero-AppBuilder/releases/latest/download/czdev-macos-aarch64 -o czdev
chmod +x czdev && sudo mv czdev /usr/local/bin/
# Login to GitHub (one-time)
czdev login
# Auto-bump patch version and publish
czdev bump --deb build/my_app_1.0.0_arm64.deb # → my_app_1.0.1_arm64.deb
czdev publish --deb build/my_app_1.0.1_arm64.deb
# Or publish directly (version in deb must be newer than existing)
czdev publish --deb build/my_app_2.0.0_arm64.deb
# Remove your own package
czdev unpublish my_app --version 1.0.1
```
## Install czdev
**Option A — Download prebuilt binary (recommended):**
| Platform | Command |
|----------|---------|
| macOS Apple Silicon | `curl -fsSL https://github.com/m5stack/CardputerZero-AppBuilder/releases/latest/download/czdev-macos-aarch64 -o czdev && chmod +x czdev && sudo mv czdev /usr/local/bin/` |
| macOS Intel | `curl -fsSL .../czdev-macos-x86_64 -o czdev && chmod +x czdev && sudo mv czdev /usr/local/bin/` |
| Linux x86_64 | `curl -fsSL .../czdev-linux-x86_64 -o czdev && chmod +x czdev && sudo mv czdev /usr/local/bin/` |
| Linux aarch64 | `curl -fsSL .../czdev-linux-aarch64 -o czdev && chmod +x czdev && sudo mv czdev /usr/local/bin/` |
| Windows | Download `czdev-windows-x86_64.exe` from [Releases](https://github.com/m5stack/CardputerZero-AppBuilder/releases) |
**Option B — Build from source:**
```bash
git clone --recursive git@github.com:m5stack/CardputerZero-AppBuilder.git
cd CardputerZero-AppBuilder
cargo build --release -p czdev
# Binary at: target/release/czdev
```
**Option C — cargo install:**
```bash
cargo install --git https://github.com/m5stack/CardputerZero-AppBuilder czdev
```
## CI Online Build
1. Go to **Actions** > **Build DEB Package** > **Run workflow**
+129
View File
@@ -0,0 +1,129 @@
use anyhow::{anyhow, Context, Result};
use std::path::{Path, PathBuf};
use std::process::Command;
const PACKAGES_INDEX_URL: &str =
"https://cardputerzero.github.io/packages/dists/stable/main/binary-arm64/Packages";
pub fn run(deb: Option<&Path>) -> Result<()> {
let deb_path = resolve_deb(deb)?;
let package = dpkg_field(&deb_path, "Package")?;
let current_ver = dpkg_field(&deb_path, "Version")?;
println!("Package: {package}");
println!("Current version in deb: {current_ver}");
// Fetch latest published version
let latest = fetch_latest_version(&package)?;
let next = match &latest {
Some(v) => {
println!("Latest published version: {v}");
bump_patch(v)
}
None => {
println!("No published version found (new package)");
bump_patch(&current_ver)
}
};
println!();
println!("Next version: {next}");
println!();
println!("To rebuild with this version, update your package control file:");
println!(" Version: {next}");
println!();
println!("Or if using CMake/packaging scripts, set:");
println!(" PKG_VERSION={next}");
Ok(())
}
fn resolve_deb(deb: Option<&Path>) -> Result<PathBuf> {
if let Some(p) = deb {
if !p.is_file() {
return Err(anyhow!("file not found: {}", p.display()));
}
return Ok(p.to_path_buf());
}
let build_dir = Path::new("build");
if build_dir.is_dir() {
let mut debs: Vec<_> = std::fs::read_dir(build_dir)?
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().map(|x| x == "deb").unwrap_or(false))
.collect();
if debs.len() == 1 {
return Ok(debs.remove(0).path());
}
if debs.len() > 1 {
return Err(anyhow!(
"multiple .deb files in build/. Specify one with --deb <path>"
));
}
}
Err(anyhow!("no .deb file found. Specify with --deb <path>"))
}
fn dpkg_field(deb: &Path, field: &str) -> Result<String> {
let output = Command::new("dpkg-deb")
.args(["-f", &deb.to_string_lossy(), field])
.output()
.with_context(|| format!("dpkg-deb -f {field}"))?;
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}
fn fetch_latest_version(package: &str) -> Result<Option<String>> {
let resp = reqwest::blocking::get(PACKAGES_INDEX_URL);
let content = match resp {
Ok(r) if r.status().is_success() => r.text().unwrap_or_default(),
_ => return Ok(None),
};
let mut in_package = false;
let mut latest: Option<String> = None;
for line in content.lines() {
if line.starts_with("Package: ") {
in_package = line.trim_start_matches("Package: ") == package;
}
if in_package && line.starts_with("Version: ") {
let ver = line.trim_start_matches("Version: ").to_string();
match &latest {
Some(existing) if compare_versions(&ver, existing) == std::cmp::Ordering::Greater => {
latest = Some(ver);
}
None => latest = Some(ver),
_ => {}
}
}
if line.is_empty() {
in_package = false;
}
}
Ok(latest)
}
fn bump_patch(version: &str) -> String {
// Handle versions like "1.0.0", "1.0", "0.2.0-1~lofibox23"
// Strip debian revision (everything after first '-')
let base = version.split('-').next().unwrap_or(version);
let parts: Vec<&str> = base.split('.').collect();
match parts.len() {
1 => format!("{}.0.1", parts[0]),
2 => format!("{}.{}.1", parts[0], parts[1]),
_ => {
let patch: u64 = parts[2].parse().unwrap_or(0);
format!("{}.{}.{}", parts[0], parts[1], patch + 1)
}
}
}
fn compare_versions(a: &str, b: &str) -> std::cmp::Ordering {
let parse = |v: &str| -> Vec<u64> {
v.split(|c: char| c == '.' || c == '-' || c == '~')
.filter_map(|s| s.parse::<u64>().ok())
.collect()
};
parse(a).cmp(&parse(b))
}
+9
View File
@@ -1,5 +1,6 @@
mod auth;
mod build;
mod bump;
mod deploy;
mod doctor;
mod github;
@@ -73,6 +74,13 @@ enum Command {
/// Remove stored GitHub credentials.
Logout,
/// Show next version (patch bump) for a package based on published versions.
Bump {
/// Path to the .deb file. If omitted, searches ./build/*.deb
#[arg(long)]
deb: Option<PathBuf>,
},
/// Publish a .deb package to the CardputerZero app store.
Publish {
/// Path to the .deb file. If omitted, searches ./build/*.deb
@@ -107,6 +115,7 @@ fn main() -> Result<()> {
Command::Deploy { path, host, deb } => deploy::run(&path, host.as_deref(), deb.as_deref()),
Command::Login => auth::login(),
Command::Logout => auth::logout(),
Command::Bump { deb } => bump::run(deb.as_deref()),
Command::Publish { deb } => publish::run(deb.as_deref()),
Command::Unpublish {
package,