Add automatic semver versioning for agent
Features: - Parse and compare semantic versions from Docker Hub - Check if current version is outdated on startup - Only pull updates when newer version is available - GitLab CI now tags images with Cargo.toml version - Created bump-version.sh script for easy version bumping How it works: 1. Cargo.toml contains source of truth version (0.1.0) 2. GitLab CI extracts version and tags Docker images with it 3. Agent queries Docker Hub for all semver tags 4. Compares current version against latest available 5. Only pulls and restarts if newer version exists Version bumping workflow: ./scripts/bump-version.sh patch # 0.1.0 -> 0.1.1 ./scripts/bump-version.sh minor # 0.1.0 -> 0.2.0 ./scripts/bump-version.sh major # 0.1.0 -> 1.0.0 This creates git commit and tag, ready to push.
This commit is contained in:
parent
f80298a2ce
commit
097c4bd581
3 changed files with 203 additions and 21 deletions
|
|
@ -66,6 +66,10 @@ build:main:
|
||||||
- echo "$DOCKERHUB_TOKEN" | docker login -u "$DOCKERHUB_USERNAME" --password-stdin
|
- 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 create --driver docker-container --name builder --use || docker buildx use builder
|
||||||
- docker buildx inspect --bootstrap
|
- 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:
|
script:
|
||||||
- |
|
- |
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
|
|
@ -73,10 +77,11 @@ build:main:
|
||||||
--cache-from type=registry,ref=$REGISTRY/$IMAGE_NAME:buildcache \
|
--cache-from type=registry,ref=$REGISTRY/$IMAGE_NAME:buildcache \
|
||||||
--cache-to type=registry,ref=$REGISTRY/$IMAGE_NAME:buildcache,mode=max \
|
--cache-to type=registry,ref=$REGISTRY/$IMAGE_NAME:buildcache,mode=max \
|
||||||
--tag $REGISTRY/$IMAGE_NAME:latest \
|
--tag $REGISTRY/$IMAGE_NAME:latest \
|
||||||
|
--tag $REGISTRY/$IMAGE_NAME:$VERSION \
|
||||||
--tag $REGISTRY/$IMAGE_NAME:main-$CI_COMMIT_SHORT_SHA \
|
--tag $REGISTRY/$IMAGE_NAME:main-$CI_COMMIT_SHORT_SHA \
|
||||||
--push \
|
--push \
|
||||||
.
|
.
|
||||||
- echo "IMAGE_TAG=latest" >> build.env
|
- echo "IMAGE_TAG=$VERSION" >> build.env
|
||||||
artifacts:
|
artifacts:
|
||||||
reports:
|
reports:
|
||||||
dotenv: build.env
|
dotenv: build.env
|
||||||
|
|
|
||||||
75
scripts/bump-version.sh
Executable file
75
scripts/bump-version.sh
Executable file
|
|
@ -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"
|
||||||
142
src/version.rs
142
src/version.rs
|
|
@ -1,36 +1,125 @@
|
||||||
use log::{info, warn};
|
use log::{info, warn};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use std::cmp::Ordering;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
const DOCKER_IMAGE: &str = "gmcintire/towerops-agent";
|
const DOCKER_IMAGE: &str = "gmcintire/towerops-agent";
|
||||||
const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION");
|
const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct DockerHubTag {
|
struct DockerHubResponse {
|
||||||
last_updated: String,
|
results: Vec<DockerHubTag>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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<Self> {
|
||||||
|
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<Ordering> {
|
||||||
|
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() {
|
pub fn check_for_updates() {
|
||||||
info!("Current version: {}", CURRENT_VERSION);
|
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
|
/// 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 already up to date
|
||||||
/// Returns Ok(true) if update was initiated, Ok(false) if pull showed no changes
|
|
||||||
pub fn perform_self_update() -> Result<bool, String> {
|
pub fn perform_self_update() -> Result<bool, String> {
|
||||||
// Check if latest tag exists on Docker Hub (quick sanity check)
|
// Check if a newer version is available
|
||||||
match check_latest_exists() {
|
match get_latest_version() {
|
||||||
Ok(last_updated) => {
|
Ok(latest) => {
|
||||||
info!(
|
let current = Version::parse(CURRENT_VERSION);
|
||||||
"Latest image on Docker Hub was updated at: {}",
|
let latest_version = Version::parse(&latest);
|
||||||
last_updated
|
|
||||||
);
|
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) => {
|
Err(e) => {
|
||||||
warn!("Could not verify latest tag on Docker Hub: {}", e);
|
warn!("Could not check Docker Hub for versions: {}", e);
|
||||||
// Continue anyway - if pull fails we'll catch it below
|
// Continue anyway - try to pull and see if anything changed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,17 +150,30 @@ pub fn perform_self_update() -> Result<bool, String> {
|
||||||
std::process::exit(0);
|
std::process::exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if the latest tag exists on Docker Hub and return its last_updated timestamp
|
/// Get the latest version from Docker Hub
|
||||||
fn check_latest_exists() -> Result<String, Box<dyn std::error::Error>> {
|
fn get_latest_version() -> Result<String, Box<dyn std::error::Error>> {
|
||||||
let url = format!(
|
let url = format!(
|
||||||
"https://hub.docker.com/v2/repositories/{}/tags/latest",
|
"https://hub.docker.com/v2/repositories/{}/tags?page_size=100",
|
||||||
DOCKER_IMAGE
|
DOCKER_IMAGE
|
||||||
);
|
);
|
||||||
|
|
||||||
let response: DockerHubTag = ureq::get(&url)
|
let response: DockerHubResponse = ureq::get(&url)
|
||||||
.timeout(std::time::Duration::from_secs(10))
|
.timeout(std::time::Duration::from_secs(10))
|
||||||
.call()?
|
.call()?
|
||||||
.into_json()?;
|
.into_json()?;
|
||||||
|
|
||||||
Ok(response.last_updated)
|
// Filter for semver tags and find the latest
|
||||||
|
let mut versions: Vec<Version> = 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())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue