Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 144 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
name: Release

on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
tag:
description: 'Release tag (e.g., v0.1.0)'
required: true
type: string

env:
CARGO_TERM_COLOR: always

jobs:
build:
name: Build ${{ matrix.target }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-gnu
os: ubuntu-latest
artifact_name: jcode-linux-x86_64
- target: aarch64-unknown-linux-gnu
os: ubuntu-latest
artifact_name: jcode-linux-aarch64
- target: x86_64-apple-darwin
os: macos-latest
artifact_name: jcode-macos-x86_64
- target: aarch64-apple-darwin
os: macos-latest
artifact_name: jcode-macos-aarch64

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install Rust
uses: dtolnay/rust-action@stable
with:
targets: ${{ matrix.target }}

- name: Install cross-compilation tools (Linux aarch64)
if: matrix.target == 'aarch64-unknown-linux-gnu'
run: |
sudo apt-get update
sudo apt-get install -y gcc-aarch64-linux-gnu

- name: Build
env:
JCODE_RELEASE_BUILD: "1"
run: |
if [ "${{ matrix.target }}" = "aarch64-unknown-linux-gnu" ]; then
export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc
fi
cargo build --release --target ${{ matrix.target }}

- name: Prepare artifact
run: |
mkdir -p artifacts
cp target/${{ matrix.target }}/release/jcode artifacts/${{ matrix.artifact_name }}
chmod +x artifacts/${{ matrix.artifact_name }}

- name: Generate checksum
run: |
cd artifacts
sha256sum ${{ matrix.artifact_name }} > ${{ matrix.artifact_name }}.sha256

- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact_name }}
path: artifacts/*

release:
name: Create Release
needs: build
runs-on: ubuntu-latest
permissions:
contents: write

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
merge-multiple: true

- name: List artifacts
run: ls -la artifacts/

- name: Get tag name
id: tag
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "tag=${{ inputs.tag }}" >> $GITHUB_OUTPUT
else
echo "tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
fi

- name: Get commit hash
id: commit
run: echo "hash=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT

- name: Generate release notes
id: notes
run: |
echo "## What's Changed" > release_notes.md
echo "" >> release_notes.md
git log --oneline -10 --format="- %s" >> release_notes.md
echo "" >> release_notes.md
echo "## Checksums" >> release_notes.md
echo '```' >> release_notes.md
cat artifacts/*.sha256 >> release_notes.md
echo '```' >> release_notes.md
echo "" >> release_notes.md
echo "**Commit:** ${{ steps.commit.outputs.hash }}" >> release_notes.md

- name: Create Release
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ steps.tag.outputs.tag }}
name: jcode ${{ steps.tag.outputs.tag }}
body_path: release_notes.md
files: |
artifacts/jcode-linux-x86_64
artifacts/jcode-linux-x86_64.sha256
artifacts/jcode-linux-aarch64
artifacts/jcode-linux-aarch64.sha256
artifacts/jcode-macos-x86_64
artifacts/jcode-macos-x86_64.sha256
artifacts/jcode-macos-aarch64
artifacts/jcode-macos-aarch64.sha256
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
6 changes: 6 additions & 0 deletions build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,18 @@ fn main() {
format!("v0.1.{} ({})", build_number, git_hash)
};

// Check if this is a release build (set by CI)
let is_release_build = std::env::var("JCODE_RELEASE_BUILD").is_ok();

// Set environment variables for compilation
println!("cargo:rustc-env=JCODE_GIT_HASH={}", git_hash);
println!("cargo:rustc-env=JCODE_GIT_DATE={}", git_date);
println!("cargo:rustc-env=JCODE_VERSION={}", version);
println!("cargo:rustc-env=JCODE_BUILD_NUMBER={}", build_number);
println!("cargo:rustc-env=JCODE_CHANGELOG={}", changelog);
if is_release_build {
println!("cargo:rustc-env=JCODE_RELEASE_BUILD=1");
}

// Re-run if git HEAD changes
println!("cargo:rerun-if-changed=.git/HEAD");
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ pub mod storage;
pub mod todo;
pub mod tool;
pub mod tui;
pub mod update;
161 changes: 161 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ mod storage;
mod todo;
mod tool;
mod tui;
mod update;

use anyhow::Result;
use clap::{Parser, Subcommand, ValueEnum};
Expand Down Expand Up @@ -221,6 +222,37 @@ async fn main() -> Result<()> {
server::set_socket_path(socket);
}

// Check for crash loop (only for release builds with auto-update enabled)
let build_info = update::BuildInfo::current();
if build_info.is_release_build {
if let Ok(true) = update::check_crash_loop() {
eprintln!("⚠️ Detected crash loop after update, attempting rollback...");
match update::rollback() {
Ok(Some(path)) => {
eprintln!("Rolled back to: {}", path.display());
// Exec into the previous version
use std::os::unix::process::CommandExt;
let args: Vec<String> = std::env::args().skip(1).collect();
let err = ProcessCommand::new(&path)
.args(&args)
.arg("--no-update")
.exec();
eprintln!("Failed to exec previous version: {}", err);
}
Ok(None) => {
eprintln!("No previous version available for rollback");
}
Err(e) => {
eprintln!("Rollback failed: {}", e);
}
}
}
// Mark startup for crash detection
if let Err(e) = update::mark_startup() {
logging::info(&format!("Warning: Failed to mark startup: {}", e));
}
}

// Check for updates unless --no-update is specified or running Update command
if !args.no_update && !matches!(args.command, Some(Command::Update)) && args.resume.is_none() {
if let Some(update_available) = check_for_updates() {
Expand Down Expand Up @@ -266,6 +298,13 @@ async fn main() -> Result<()> {
return Err(e);
}

// Successful startup - clear crash marker (for release builds)
if update::BuildInfo::current().is_release_build {
if let Err(e) = update::mark_startup_success() {
logging::info(&format!("Warning: Failed to clear crash marker: {}", e));
}
}

Ok(())
}

Expand Down Expand Up @@ -1159,7 +1198,27 @@ pub fn main_get_repo_dir() -> Option<std::path::PathBuf> {

/// Check if updates are available (returns None if unable to check)
/// Only returns true if remote is AHEAD of local (not if local is ahead)
///
/// Two modes:
/// 1. Developer mode: binary is in git repo, check for new commits
/// 2. Release mode: binary is a release build, check GitHub Releases
fn check_for_updates() -> Option<bool> {
// For release builds, use async check (but we're in sync context)
// So we do a quick synchronous check here and defer actual download
let build_info = update::BuildInfo::current();

if build_info.is_release_build {
// Release build: check metadata to see if we should check GitHub
let metadata = update::UpdateMetadata::load().ok()?;
if !metadata.should_check() {
return Some(false);
}
// For release builds, we'll do the actual async check in run_auto_update
// Here we just signal that a check should be attempted
return Some(true);
}

// Developer mode: use git-based check
let repo_dir = get_repo_dir()?;

// Fetch quietly
Expand Down Expand Up @@ -1193,9 +1252,21 @@ fn check_for_updates() -> Option<bool> {
}

/// Auto-update: pull, build, and exec into new binary
///
/// Two modes:
/// 1. Developer mode: git pull, cargo build, exec into new binary
/// 2. Release mode: download from GitHub Releases, exec into new binary
fn run_auto_update() -> Result<()> {
use std::os::unix::process::CommandExt;

let build_info = update::BuildInfo::current();

if build_info.is_release_build {
// Release mode: download from GitHub Releases
return run_release_auto_update();
}

// Developer mode: git pull + cargo build
let repo_dir =
get_repo_dir().ok_or_else(|| anyhow::anyhow!("Could not find jcode repository"))?;

Expand Down Expand Up @@ -1244,8 +1315,64 @@ fn run_auto_update() -> Result<()> {
Err(anyhow::anyhow!("Failed to exec new binary: {}", err))
}

/// Auto-update for release builds: download from GitHub Releases
fn run_release_auto_update() -> Result<()> {
use std::os::unix::process::CommandExt;

// Create a small tokio runtime for the async operations
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?;

let new_binary = rt.block_on(async {
// Check for update
let release = match update::check_for_update().await {
Ok(Some(release)) => release,
Ok(None) => {
// Update last check time even if no update
let mut metadata = update::UpdateMetadata::load().unwrap_or_default();
metadata.last_check = std::time::SystemTime::now();
let _ = metadata.save();
anyhow::bail!("No update available");
}
Err(e) => anyhow::bail!("Failed to check for update: {}", e),
};

eprintln!(
"Downloading {} from GitHub...",
release.tag_name
);

// Download and install
update::download_and_install(&release).await
})?;

eprintln!("Updated to {}. Restarting...", new_binary.display());

// Exec into new binary with same args
let args: Vec<String> = std::env::args().skip(1).collect();

let err = ProcessCommand::new(&new_binary)
.args(&args)
.arg("--no-update") // Prevent infinite update loop
.exec();

Err(anyhow::anyhow!("Failed to exec new binary: {}", err))
}

/// Run the update process (manual)
///
/// Two modes:
/// 1. Developer mode: git pull + cargo build
/// 2. Release mode: download from GitHub Releases
fn run_update() -> Result<()> {
let build_info = update::BuildInfo::current();

if build_info.is_release_build {
return run_release_update();
}

// Developer mode
let repo_dir =
get_repo_dir().ok_or_else(|| anyhow::anyhow!("Could not find jcode repository"))?;

Expand Down Expand Up @@ -1289,6 +1416,40 @@ fn run_update() -> Result<()> {
Ok(())
}

/// Run update for release builds: download from GitHub Releases
fn run_release_update() -> Result<()> {
let build_info = update::BuildInfo::current();
eprintln!("Current version: {} ({})", build_info.version, build_info.git_hash);
eprintln!("Checking for updates from GitHub...");

// Create a small tokio runtime for the async operations
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?;

rt.block_on(async {
// Fetch latest release
let release = update::fetch_latest_release().await?;

eprintln!("Latest release: {}", release.tag_name);

// Check if we need to update
match update::check_for_update().await? {
Some(release) => {
eprintln!("Downloading {}...", release.tag_name);
let path = update::download_and_install(&release).await?;
eprintln!("Successfully updated to: {}", path.display());
eprintln!("\nRestart jcode to use the new version.");
Ok(())
}
None => {
eprintln!("Already up to date!");
Ok(())
}
}
})
}

/// List available sessions for resume - interactive picker
fn list_sessions() -> Result<()> {
match tui::session_picker::pick_session()? {
Expand Down
Loading