diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c336042..a3cb52a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -66,6 +66,10 @@ build:main: - echo "$DOCKERHUB_TOKEN" | docker login -u "$DOCKERHUB_USERNAME" --password-stdin - docker buildx create --driver docker-container --name builder --use || docker buildx use builder - docker buildx inspect --bootstrap + # Extract version from Cargo.toml + - apk add --no-cache grep sed + - VERSION=$(grep '^version = ' Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/') + - echo "Building version $VERSION" script: - | docker buildx build \ @@ -73,10 +77,11 @@ build:main: --cache-from type=registry,ref=$REGISTRY/$IMAGE_NAME:buildcache \ --cache-to type=registry,ref=$REGISTRY/$IMAGE_NAME:buildcache,mode=max \ --tag $REGISTRY/$IMAGE_NAME:latest \ + --tag $REGISTRY/$IMAGE_NAME:$VERSION \ --tag $REGISTRY/$IMAGE_NAME:main-$CI_COMMIT_SHORT_SHA \ --push \ . - - echo "IMAGE_TAG=latest" >> build.env + - echo "IMAGE_TAG=$VERSION" >> build.env artifacts: reports: dotenv: build.env diff --git a/scripts/bump-version.sh b/scripts/bump-version.sh new file mode 100755 index 0000000..4aefb55 --- /dev/null +++ b/scripts/bump-version.sh @@ -0,0 +1,75 @@ +#!/bin/bash + +# Bump semantic version in Cargo.toml +# Usage: ./bump-version.sh [major|minor|patch] + +set -e + +BUMP_TYPE=${1:-patch} + +if [[ ! "$BUMP_TYPE" =~ ^(major|minor|patch)$ ]]; then + echo "Error: Invalid bump type. Use: major, minor, or patch" + exit 1 +fi + +# Get current version from Cargo.toml +CURRENT_VERSION=$(grep '^version = ' Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/') + +if [ -z "$CURRENT_VERSION" ]; then + echo "Error: Could not find version in Cargo.toml" + exit 1 +fi + +echo "Current version: $CURRENT_VERSION" + +# Parse version +IFS='.' read -r major minor patch <<< "$CURRENT_VERSION" + +# Bump version +case $BUMP_TYPE in + major) + major=$((major + 1)) + minor=0 + patch=0 + ;; + minor) + minor=$((minor + 1)) + patch=0 + ;; + patch) + patch=$((patch + 1)) + ;; +esac + +NEW_VERSION="$major.$minor.$patch" + +echo "New version: $NEW_VERSION" + +# Update Cargo.toml +if [[ "$OSTYPE" == "darwin"* ]]; then + # macOS + sed -i '' "s/^version = \".*\"/version = \"$NEW_VERSION\"/" Cargo.toml +else + # Linux + sed -i "s/^version = \".*\"/version = \"$NEW_VERSION\"/" Cargo.toml +fi + +echo "✓ Updated Cargo.toml" + +# Update Cargo.lock +cargo check --quiet +echo "✓ Updated Cargo.lock" + +# Git operations +git add Cargo.toml Cargo.lock +git commit -m "Bump version to $NEW_VERSION" +git tag "v$NEW_VERSION" + +echo "" +echo "✓ Version bumped to $NEW_VERSION" +echo "" +echo "Next steps:" +echo " git push origin main" +echo " git push origin v$NEW_VERSION" +echo "" +echo "This will trigger a GitLab CI build that tags the Docker image with $NEW_VERSION" diff --git a/src/version.rs b/src/version.rs index 879e01d..a3f4fdc 100644 --- a/src/version.rs +++ b/src/version.rs @@ -1,36 +1,125 @@ use log::{info, warn}; use serde::Deserialize; +use std::cmp::Ordering; use std::process::Command; const DOCKER_IMAGE: &str = "gmcintire/towerops-agent"; const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION"); #[derive(Debug, Deserialize)] -struct DockerHubTag { - last_updated: String, +struct DockerHubResponse { + results: Vec, } -/// Simple startup check - just logs current version and that updates are automatic +#[derive(Debug, Deserialize)] +struct DockerHubTag { + name: String, +} + +#[derive(Debug, PartialEq, Eq)] +struct Version { + major: u32, + minor: u32, + patch: u32, +} + +impl Version { + fn parse(s: &str) -> Option { + let s = s.strip_prefix('v').unwrap_or(s); + let parts: Vec<&str> = s.split('.').collect(); + + if parts.len() != 3 { + return None; + } + + Some(Version { + major: parts[0].parse().ok()?, + minor: parts[1].parse().ok()?, + patch: parts[2].parse().ok()?, + }) + } +} + +impl PartialOrd for Version { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Version { + fn cmp(&self, other: &Self) -> Ordering { + match self.major.cmp(&other.major) { + Ordering::Equal => match self.minor.cmp(&other.minor) { + Ordering::Equal => self.patch.cmp(&other.patch), + other => other, + }, + other => other, + } + } +} + +/// Startup check - logs current version and checks for updates pub fn check_for_updates() { info!("Current version: {}", CURRENT_VERSION); - info!("Automatic updates enabled - will check every hour"); + + match get_latest_version() { + Ok(latest) => { + let current = Version::parse(CURRENT_VERSION); + let latest_version = Version::parse(&latest); + + match (current, latest_version) { + (Some(curr), Some(lat)) => { + if lat > curr { + warn!( + "⚠️ Newer version available: {} (current: {})", + latest, CURRENT_VERSION + ); + warn!(" Automatic updates will pull new version every hour"); + } else { + info!("✓ Running latest version ({})", CURRENT_VERSION); + } + } + _ => { + info!("Could not compare versions, automatic updates enabled"); + } + } + } + Err(e) => { + warn!("Could not check for updates: {}", e); + info!("Automatic updates enabled - will check every hour"); + } + } } /// Perform self-update by pulling latest image and exiting -/// This always pulls latest since we use the :latest tag and can't reliably compare versions -/// Returns Ok(true) if update was initiated, Ok(false) if pull showed no changes +/// Returns Ok(true) if update was initiated, Ok(false) if already up to date pub fn perform_self_update() -> Result { - // Check if latest tag exists on Docker Hub (quick sanity check) - match check_latest_exists() { - Ok(last_updated) => { - info!( - "Latest image on Docker Hub was updated at: {}", - last_updated - ); + // Check if a newer version is available + match get_latest_version() { + Ok(latest) => { + let current = Version::parse(CURRENT_VERSION); + let latest_version = Version::parse(&latest); + + match (current, latest_version) { + (Some(curr), Some(lat)) => { + if lat <= curr { + info!( + "Already running latest version ({} <= {})", + latest, CURRENT_VERSION + ); + return Ok(false); + } + + info!("Update available: {} -> {}", CURRENT_VERSION, latest); + } + _ => { + warn!("Could not parse versions, proceeding with update check"); + } + } } Err(e) => { - warn!("Could not verify latest tag on Docker Hub: {}", e); - // Continue anyway - if pull fails we'll catch it below + warn!("Could not check Docker Hub for versions: {}", e); + // Continue anyway - try to pull and see if anything changed } } @@ -61,17 +150,30 @@ pub fn perform_self_update() -> Result { std::process::exit(0); } -/// Check if the latest tag exists on Docker Hub and return its last_updated timestamp -fn check_latest_exists() -> Result> { +/// Get the latest version from Docker Hub +fn get_latest_version() -> Result> { let url = format!( - "https://hub.docker.com/v2/repositories/{}/tags/latest", + "https://hub.docker.com/v2/repositories/{}/tags?page_size=100", DOCKER_IMAGE ); - let response: DockerHubTag = ureq::get(&url) + let response: DockerHubResponse = ureq::get(&url) .timeout(std::time::Duration::from_secs(10)) .call()? .into_json()?; - Ok(response.last_updated) + // Filter for semver tags and find the latest + let mut versions: Vec = response + .results + .iter() + .filter_map(|tag| Version::parse(&tag.name)) + .collect(); + + versions.sort(); + versions.reverse(); // Highest version first + + versions + .first() + .map(|v| format!("{}.{}.{}", v.major, v.minor, v.patch)) + .ok_or_else(|| "No valid semver tags found".into()) }