Compare commits

...
4 Commits
Author SHA1 Message Date
leeguooooo e8864c96e2 chore(release): 1.2.2 — non-blocking 'update available' notice + release changelogs
Release binaries / Build macOS ARM64 (push) Has been cancelled
Release binaries / Build macOS x64 (push) Has been cancelled
Release binaries / Build Linux ARM64 (push) Has been cancelled
Release binaries / Build Linux musl ARM64 (push) Has been cancelled
Release binaries / Build Linux musl x64 (push) Has been cancelled
Release binaries / Build Linux x64 (push) Has been cancelled
Release binaries / Build Windows x64 (push) Has been cancelled
Release binaries / Attach binaries to GitHub Release (push) Has been cancelled
- feat(cli): non-blocking update-available notice (stderr, once/day, opt-out) so users learn to upgrade
- ci(release): auto-generate changelog from commit log on every release
2026-06-12 22:47:03 +09:00
leeguooooo 8432f6cd69 feat(cli): non-blocking 'update available' notice so users know to upgrade
The CLI ships as a GitHub Release binary with a manual `chrome-use upgrade`,
but nothing told users a newer version existed — so releases didn't reach them.

Add a lightweight update check: each run reads a cached latest-version and, if
it's newer than the running binary, prints a one-line hint to STDERR (never
stdout, so --json stays clean): "⚠ chrome-use X.Y.Z is available — run
chrome-use upgrade". The cache is refreshed at most once a day by a DETACHED
`__update-check` worker (curl → GitHub latest release), so the current command
never waits on the network. Skipped for meta commands (upgrade/install/doctor/
__*/--version/--help), in CI, in daemon mode, and via
CHROME_USE_NO_UPDATE_CHECK / AGENT_BROWSER_NO_UPDATE_CHECK.

Verified: nag shows for a newer cached version, suppressed by the opt-out env +
on meta commands + when up-to-date; the detached worker writes the real latest
tag from the GitHub API.
2026-06-12 22:46:44 +09:00
leeguooooo a8089310a6 ci(release): build changelog from commit log (not PR-only notes)
GitHub's generate_release_notes only lists merged PRs — near-empty for this
commit-to-main repo, so releases still showed nothing. Render the
conventional-commit subjects since the previous tag instead, and full-clone
(fetch-depth:0) so the diff is available.
2026-06-12 18:18:11 +09:00
leeguooooo ab92d2590b ci(release): auto-generate release changelog (commits + merged PRs since last tag)
GitHub Releases had an empty body — you couldn't tell what changed between
versions. Add generate_release_notes:true so every release ships an
auto-generated changelog.
2026-06-12 18:15:18 +09:00
6 changed files with 182 additions and 5 deletions
+28 -1
View File
@@ -39,6 +39,7 @@ jobs:
uses: actions/checkout@v6 uses: actions/checkout@v6
with: with:
ref: ${{ github.event.inputs.tag || github.ref }} ref: ${{ github.event.inputs.tag || github.ref }}
fetch-depth: 0 # full history so the changelog step can diff tags
- name: Setup Rust toolchain - name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
@@ -125,6 +126,29 @@ jobs:
- name: List assets - name: List assets
run: ls -la dist run: ls -la dist
# Build the changelog from conventional-commit subjects since the previous
# tag. GitHub's built-in generate_release_notes only lists merged PRs,
# which is near-empty for this commit-to-main workflow — so we render the
# commit log ourselves and every release shows what actually changed.
- name: Generate changelog
id: changelog
run: |
TAG="${{ github.event.inputs.tag || github.ref_name }}"
PREV="$(git describe --tags --abbrev=0 "${TAG}^" 2>/dev/null || true)"
{
echo "notes<<__NOTES_EOF__"
echo "## What changed"
echo ""
if [ -n "$PREV" ]; then
git log "${PREV}..${TAG}" --no-merges --pretty='- %s' | grep -v '^- chore(release)' || true
echo ""
echo "**Full changelog**: https://github.com/${{ github.repository }}/compare/${PREV}...${TAG}"
else
git log "${TAG}" --no-merges --pretty='- %s' | grep -v '^- chore(release)' || true
fi
echo "__NOTES_EOF__"
} >> "$GITHUB_OUTPUT"
- name: Attach to release - name: Attach to release
uses: softprops/action-gh-release@v3 uses: softprops/action-gh-release@v3
with: with:
@@ -133,5 +157,8 @@ jobs:
dist/*.tar.gz dist/*.tar.gz
dist/*.tar.gz.sha256 dist/*.tar.gz.sha256
fail_on_unmatched_files: true fail_on_unmatched_files: true
# keep existing release notes if the release was created beforehand # The commit-based changelog so every release shows what changed. The
# first matrix job to run creates the release with these notes;
# append_body:false keeps later platform jobs from duplicating them.
body: ${{ steps.changelog.outputs.notes }}
append_body: false append_body: false
+1 -1
View File
@@ -290,7 +290,7 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]] [[package]]
name = "chrome-use" name = "chrome-use"
version = "1.2.1" version = "1.2.2"
dependencies = [ dependencies = [
"aes", "aes",
"aes-gcm", "aes-gcm",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "chrome-use" name = "chrome-use"
version = "1.2.1" version = "1.2.2"
edition = "2021" edition = "2021"
description = "Fast browser automation CLI for AI agents" description = "Fast browser automation CLI for AI agents"
license = "Apache-2.0" license = "Apache-2.0"
+11
View File
@@ -586,6 +586,17 @@ fn main() {
return; return;
} }
// Hidden update-check worker, spawned detached by maybe_notify_update() to
// refresh the cached latest version without blocking a real command.
if env::args().nth(1).as_deref() == Some("__update-check") {
upgrade::run_update_check();
return;
}
// Non-blocking "update available" hint (stderr only; self-skips meta
// commands, daemon mode, CI, and the opt-out env vars).
upgrade::maybe_notify_update();
// Native daemon mode: when AGENT_BROWSER_DAEMON is set, run as the daemon process // Native daemon mode: when AGENT_BROWSER_DAEMON is set, run as the daemon process
if env::var("AGENT_BROWSER_DAEMON").is_ok() { if env::var("AGENT_BROWSER_DAEMON").is_ok() {
// Ignore SIGPIPE so the daemon isn't killed when the parent drops // Ignore SIGPIPE so the daemon isn't killed when the parent drops
+140 -1
View File
@@ -1,5 +1,7 @@
use crate::color; use crate::color;
use std::process::{exit, Command}; use std::path::PathBuf;
use std::process::{exit, Command, Stdio};
use std::time::{SystemTime, UNIX_EPOCH};
const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION"); const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION");
@@ -7,6 +9,143 @@ const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION");
/// upgrade path and the install path are identical (GitHub Release, no npm). /// upgrade path and the install path are identical (GitHub Release, no npm).
const INSTALL_URL: &str = "https://raw.githubusercontent.com/leeguooooo/chrome-use/main/install.sh"; const INSTALL_URL: &str = "https://raw.githubusercontent.com/leeguooooo/chrome-use/main/install.sh";
/// GitHub API for the latest published release (used by the update check).
const LATEST_RELEASE_API: &str =
"https://api.github.com/repos/leeguooooo/chrome-use/releases/latest";
/// Re-check the latest version at most this often (seconds).
const UPDATE_CHECK_INTERVAL_SECS: u64 = 86_400; // once a day
fn now_secs() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
fn update_cache_path() -> PathBuf {
crate::connection::config_home().join("update-check.json")
}
fn write_update_cache(checked_at: u64, latest: &str) {
let path = update_cache_path();
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let body = serde_json::json!({ "checked_at": checked_at, "latest": latest }).to_string();
let _ = std::fs::write(&path, body);
}
/// Parse a dotted version (`1.2.1`, `v1.2.1`, `1.2.1-fork.3`) into a comparable
/// `(major, minor, patch)`, ignoring any pre-release/build suffix.
fn parse_version(v: &str) -> Option<(u64, u64, u64)> {
let core = v.trim().trim_start_matches('v');
let core = core.split(['-', '+']).next().unwrap_or(core);
let mut parts = core.split('.');
let major = parts.next()?.parse().ok()?;
let minor = parts.next().unwrap_or("0").parse().ok()?;
let patch = parts.next().unwrap_or("0").parse().ok()?;
Some((major, minor, patch))
}
fn is_newer(latest: &str, current: &str) -> bool {
matches!((parse_version(latest), parse_version(current)), (Some(l), Some(c)) if l > c)
}
/// Hidden `__update-check` subcommand: fetch the latest release tag and cache it.
/// Spawned detached by [`maybe_notify_update`] so the network call never blocks a
/// real command. Uses `curl` (no extra deps, matches `upgrade`).
pub fn run_update_check() {
let latest = Command::new("curl")
.args([
"-fsSL",
"--max-time",
"8",
"-H",
"User-Agent: chrome-use-update-check",
LATEST_RELEASE_API,
])
.output()
.ok()
.filter(|o| o.status.success())
.and_then(|o| serde_json::from_slice::<serde_json::Value>(&o.stdout).ok())
.and_then(|j| {
j.get("tag_name")
.and_then(|v| v.as_str())
.map(|s| s.trim_start_matches('v').to_string())
});
if let Some(latest) = latest {
write_update_cache(now_secs(), &latest);
}
}
/// Non-blocking "update available" notice. Called once per command run:
/// - prints a one-line hint to **stderr** (never stdout, so `--json` is clean)
/// when a cached release is newer than the running binary;
/// - refreshes the cached latest version at most once a day via a **detached**
/// background process, so the current command never waits on the network.
///
/// Skipped for meta commands (upgrade/install/doctor/`__*`/--version/--help),
/// in CI, in daemon mode, and when CHROME_USE_NO_UPDATE_CHECK /
/// AGENT_BROWSER_NO_UPDATE_CHECK is set.
pub fn maybe_notify_update() {
if std::env::var_os("CHROME_USE_NO_UPDATE_CHECK").is_some()
|| std::env::var_os("AGENT_BROWSER_NO_UPDATE_CHECK").is_some()
|| std::env::var_os("CI").is_some()
|| std::env::var_os("AGENT_BROWSER_DAEMON").is_some()
{
return;
}
let first = std::env::args().nth(1).unwrap_or_default();
if first.starts_with("__")
|| matches!(
first.as_str(),
"upgrade" | "install" | "doctor" | "dashboard"
)
{
return;
}
if std::env::args().any(|a| matches!(a.as_str(), "--version" | "-V" | "--help" | "-h")) {
return;
}
let (checked_at, latest) = std::fs::read_to_string(update_cache_path())
.ok()
.and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
.map(|j| {
(
j.get("checked_at").and_then(|v| v.as_u64()).unwrap_or(0),
j.get("latest")
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string(),
)
})
.unwrap_or((0, String::new()));
if is_newer(&latest, CURRENT_VERSION) {
eprintln!(
"{} chrome-use {latest} is available (you have {CURRENT_VERSION}) — run `chrome-use upgrade`",
color::warning_indicator()
);
}
// Refresh in the background at most once a day. Bump the timestamp first
// (keeping the last-known latest) so concurrent runs don't all spawn a
// checker, then fire a detached child that does the network fetch.
if now_secs().saturating_sub(checked_at) >= UPDATE_CHECK_INTERVAL_SECS {
write_update_cache(now_secs(), &latest);
if let Ok(exe) = std::env::current_exe() {
let _ = Command::new(exe)
.arg("__update-check")
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn();
}
}
}
/// Upgrade to the latest GitHub Release. /// Upgrade to the latest GitHub Release.
/// ///
/// The stealth fork ships as a prebuilt binary attached to a GitHub Release — /// The stealth fork ships as a prebuilt binary attached to a GitHub Release —
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "chrome-use", "name": "chrome-use",
"version": "1.2.1", "version": "1.2.2",
"description": "chrome-use — drive your real, logged-in Chrome from any AI agent, stealth by default", "description": "chrome-use — drive your real, logged-in Chrome from any AI agent, stealth by default",
"type": "module", "type": "module",
"packageManager": "pnpm@11.1.3", "packageManager": "pnpm@11.1.3",