mirror of
https://github.com/rustfs/rustfs.git
synced 2026-01-17 01:30:33 +00:00
Compare commits
17 Commits
1.0.0-alph
...
1.0.0-alph
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4f87a4fee | ||
|
|
ee5f94a2e2 | ||
|
|
9c3cf554d3 | ||
|
|
addbfa5487 | ||
|
|
5eb461d7b7 | ||
|
|
1ea45afcd7 | ||
|
|
dbd86f6aee | ||
|
|
af693f7b3f | ||
|
|
3be5ee6445 | ||
|
|
0acc8fe26a | ||
|
|
ecf40eb86c | ||
|
|
48ce7055f8 | ||
|
|
749f55d688 | ||
|
|
e5d17f5382 | ||
|
|
982cc66c74 | ||
|
|
74bf4909c8 | ||
|
|
9c956b4445 |
38
.github/workflows/build.yml
vendored
38
.github/workflows/build.yml
vendored
@@ -12,6 +12,18 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# Build and Release Workflow
|
||||
#
|
||||
# This workflow builds RustFS binaries and automatically triggers Docker image builds.
|
||||
#
|
||||
# Flow:
|
||||
# 1. Build binaries for multiple platforms
|
||||
# 2. Upload binaries to OSS storage
|
||||
# 3. Trigger docker.yml to build and push images using the uploaded binaries
|
||||
#
|
||||
# Manual Parameters:
|
||||
# - build_docker: Build and push Docker images (default: true)
|
||||
|
||||
name: Build and Release
|
||||
|
||||
on:
|
||||
@@ -52,10 +64,10 @@ on:
|
||||
- cron: "0 0 * * 0" # Weekly on Sunday at midnight UTC
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
force_build:
|
||||
description: "Force build even without changes"
|
||||
build_docker:
|
||||
description: "Build and push Docker images after binary build"
|
||||
required: false
|
||||
default: false
|
||||
default: true
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
@@ -117,7 +129,6 @@ jobs:
|
||||
echo "🛠️ Development build detected"
|
||||
elif [[ "${{ github.event_name }}" == "schedule" ]] || \
|
||||
[[ "${{ github.event_name }}" == "workflow_dispatch" ]] || \
|
||||
[[ "${{ github.event.inputs.force_build }}" == "true" ]] || \
|
||||
[[ "${{ contains(github.event.head_commit.message, '--build') }}" == "true" ]]; then
|
||||
# Scheduled or manual build
|
||||
should_build=true
|
||||
@@ -231,7 +242,7 @@ jobs:
|
||||
cargo install cross --git https://github.com/cross-rs/cross
|
||||
cross build --release --target ${{ matrix.target }} -p rustfs --bins
|
||||
else
|
||||
# Use zigbuild for Linux ARM64
|
||||
# Use zigbuild for other cross-compilation
|
||||
cargo zigbuild --release --target ${{ matrix.target }} -p rustfs --bins
|
||||
fi
|
||||
else
|
||||
@@ -445,6 +456,13 @@ jobs:
|
||||
echo "🔢 Version: $VERSION"
|
||||
echo ""
|
||||
|
||||
# Check build status
|
||||
BUILD_STATUS="${{ needs.build-rustfs.result }}"
|
||||
|
||||
echo "📊 Build Results:"
|
||||
echo " 📦 All platforms: $BUILD_STATUS"
|
||||
echo ""
|
||||
|
||||
case "$BUILD_TYPE" in
|
||||
"development")
|
||||
echo "🛠️ Development build artifacts have been uploaded to OSS dev directory"
|
||||
@@ -461,3 +479,13 @@ jobs:
|
||||
echo "🏷️ GitHub Release will be created automatically by the release workflow"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
echo "🐳 Docker Images:"
|
||||
if [[ "${{ github.event.inputs.build_docker }}" == "false" ]]; then
|
||||
echo "⏭️ Docker image build was skipped (binary only build)"
|
||||
elif [[ "$BUILD_STATUS" == "success" ]]; then
|
||||
echo "🔄 Docker images will be built and pushed automatically via workflow_run event"
|
||||
else
|
||||
echo "❌ Docker image build will be skipped due to build failure"
|
||||
fi
|
||||
|
||||
363
.github/workflows/docker.yml
vendored
363
.github/workflows/docker.yml
vendored
@@ -12,42 +12,34 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# Docker Images Workflow
|
||||
#
|
||||
# This workflow builds Docker images using pre-built binaries from the build workflow.
|
||||
#
|
||||
# Trigger Types:
|
||||
# 1. workflow_run: Automatically triggered when "Build and Release" workflow completes
|
||||
# 2. workflow_dispatch: Manual trigger for standalone Docker builds
|
||||
#
|
||||
# Key Features:
|
||||
# - Only triggers when Linux builds (x86_64 + aarch64) are successful
|
||||
# - Independent of macOS/Windows build status
|
||||
# - Uses workflow_run event for precise control
|
||||
# - Only builds Docker images for releases and prereleases (development builds are skipped)
|
||||
|
||||
name: Docker Images
|
||||
|
||||
# Permissions needed for workflow_run event and Docker registry access
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ["*.*.*"]
|
||||
# Automatically triggered when build workflow completes
|
||||
workflow_run:
|
||||
workflows: ["Build and Release"]
|
||||
types: [completed]
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- "**.md"
|
||||
- "**.txt"
|
||||
- ".github/**"
|
||||
- "docs/**"
|
||||
- "deploy/**"
|
||||
- "scripts/dev_*.sh"
|
||||
- "LICENSE*"
|
||||
- "README*"
|
||||
- "**/*.png"
|
||||
- "**/*.jpg"
|
||||
- "**/*.svg"
|
||||
- ".gitignore"
|
||||
- ".dockerignore"
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- "**.md"
|
||||
- "**.txt"
|
||||
- ".github/**"
|
||||
- "docs/**"
|
||||
- "deploy/**"
|
||||
- "scripts/dev_*.sh"
|
||||
- "LICENSE*"
|
||||
- "README*"
|
||||
- "**/*.png"
|
||||
- "**/*.jpg"
|
||||
- "**/*.svg"
|
||||
- ".gitignore"
|
||||
- ".dockerignore"
|
||||
# Manual trigger with same parameters for consistency
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
push_images:
|
||||
@@ -56,9 +48,9 @@ on:
|
||||
default: true
|
||||
type: boolean
|
||||
version:
|
||||
description: "Version to build (latest, main-latest, dev-latest, or specific version like v1.0.0 or dev-abc123)"
|
||||
description: "Version to build (latest for stable release, or specific version like v1.0.0, v1.0.0-alpha1)"
|
||||
required: false
|
||||
default: "main-latest"
|
||||
default: "latest"
|
||||
type: string
|
||||
force_rebuild:
|
||||
description: "Force rebuild even if binary exists (useful for testing)"
|
||||
@@ -67,12 +59,14 @@ on:
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
DOCKERHUB_USERNAME: rustfs
|
||||
CARGO_TERM_COLOR: always
|
||||
REGISTRY_DOCKERHUB: rustfs/rustfs
|
||||
REGISTRY_GHCR: ghcr.io/${{ github.repository }}
|
||||
DOCKER_PLATFORMS: linux/amd64,linux/arm64
|
||||
|
||||
jobs:
|
||||
# Docker build strategy check
|
||||
# Check if we should build Docker images
|
||||
build-check:
|
||||
name: Docker Build Check
|
||||
runs-on: ubuntu-latest
|
||||
@@ -101,26 +95,73 @@ jobs:
|
||||
is_prerelease=false
|
||||
create_latest=false
|
||||
|
||||
# Get short SHA for all builds
|
||||
short_sha=$(git rev-parse --short HEAD)
|
||||
if [[ "${{ github.event_name }}" == "workflow_run" ]]; then
|
||||
# Triggered by build workflow completion
|
||||
echo "🔗 Triggered by build workflow completion"
|
||||
|
||||
# Always build on workflow_dispatch or when changes detected
|
||||
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]] || \
|
||||
[[ "${{ github.event_name }}" == "push" ]] || \
|
||||
[[ "${{ github.event_name }}" == "pull_request" ]]; then
|
||||
should_build=true
|
||||
fi
|
||||
# Check if the triggering workflow was successful
|
||||
# If the workflow succeeded, it means ALL builds (including Linux x86_64 and aarch64) succeeded
|
||||
if [[ "${{ github.event.workflow_run.conclusion }}" == "success" ]]; then
|
||||
echo "✅ Build workflow succeeded, all builds including Linux are successful"
|
||||
should_build=true
|
||||
should_push=true
|
||||
else
|
||||
echo "❌ Build workflow failed (conclusion: ${{ github.event.workflow_run.conclusion }}), skipping Docker build"
|
||||
should_build=false
|
||||
fi
|
||||
|
||||
# Determine build type and version
|
||||
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]] && [[ -n "${{ github.event.inputs.version }}" ]]; then
|
||||
# Manual trigger with version input
|
||||
# Extract version info from commit message or use commit SHA
|
||||
# Use Git to generate consistent short SHA (ensures uniqueness like build.yml)
|
||||
short_sha=$(git rev-parse --short "${{ github.event.workflow_run.head_sha }}")
|
||||
|
||||
# Determine build type based on branch and commit
|
||||
if [[ "${{ github.event.workflow_run.head_branch }}" == "main" ]]; then
|
||||
build_type="development"
|
||||
version="dev-${short_sha}"
|
||||
# Skip Docker build for development builds
|
||||
should_build=false
|
||||
echo "⏭️ Skipping Docker build for development version (main branch)"
|
||||
elif [[ "${{ github.event.workflow_run.event }}" == "push" ]] && [[ "${{ github.event.workflow_run.head_branch }}" =~ ^refs/tags/ ]]; then
|
||||
# Tag push - only build for releases and prereleases
|
||||
tag_name="${{ github.event.workflow_run.head_branch }}"
|
||||
version="${tag_name#refs/tags/}"
|
||||
if [[ "$version" == *"alpha"* ]] || [[ "$version" == *"beta"* ]] || [[ "$version" == *"rc"* ]]; then
|
||||
build_type="prerelease"
|
||||
is_prerelease=true
|
||||
echo "🧪 Building Docker image for prerelease: $version"
|
||||
else
|
||||
build_type="release"
|
||||
create_latest=true
|
||||
echo "🚀 Building Docker image for release: $version"
|
||||
fi
|
||||
else
|
||||
build_type="development"
|
||||
version="dev-${short_sha}"
|
||||
# Skip Docker build for development builds
|
||||
should_build=false
|
||||
echo "⏭️ Skipping Docker build for development version"
|
||||
fi
|
||||
|
||||
echo "🔄 Build triggered by workflow_run:"
|
||||
echo " 📋 Conclusion: ${{ github.event.workflow_run.conclusion }}"
|
||||
echo " 🌿 Branch: ${{ github.event.workflow_run.head_branch }}"
|
||||
echo " 📎 SHA: ${{ github.event.workflow_run.head_sha }}"
|
||||
echo " 🎯 Event: ${{ github.event.workflow_run.event }}"
|
||||
|
||||
elif [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
|
||||
# Manual trigger
|
||||
input_version="${{ github.event.inputs.version }}"
|
||||
version="${input_version}"
|
||||
force_rebuild="${{ github.event.inputs.force_rebuild }}"
|
||||
should_push="${{ github.event.inputs.push_images }}"
|
||||
should_build=true
|
||||
|
||||
# Get short SHA
|
||||
short_sha=$(git rev-parse --short HEAD)
|
||||
|
||||
echo "🎯 Manual Docker build triggered:"
|
||||
echo " 📋 Requested version: $input_version"
|
||||
echo " 🔧 Force rebuild: $force_rebuild"
|
||||
echo " 🔧 Force rebuild: ${{ github.event.inputs.force_rebuild }}"
|
||||
echo " 🚀 Push images: $should_push"
|
||||
|
||||
case "$input_version" in
|
||||
"latest")
|
||||
@@ -128,16 +169,6 @@ jobs:
|
||||
create_latest=true
|
||||
echo "🚀 Building with latest stable release version"
|
||||
;;
|
||||
"main-latest")
|
||||
build_type="development"
|
||||
version="main-latest"
|
||||
echo "🛠️ Building with main branch latest development version"
|
||||
;;
|
||||
"dev-latest")
|
||||
build_type="development"
|
||||
version="dev-latest"
|
||||
echo "🛠️ Building with development latest version"
|
||||
;;
|
||||
v[0-9]*)
|
||||
build_type="release"
|
||||
create_latest=true
|
||||
@@ -148,48 +179,13 @@ jobs:
|
||||
is_prerelease=true
|
||||
echo "🧪 Building with prerelease version: $input_version"
|
||||
;;
|
||||
dev-[a-f0-9]*)
|
||||
build_type="development"
|
||||
echo "🔧 Building with specific development version: $input_version"
|
||||
;;
|
||||
*)
|
||||
build_type="development"
|
||||
echo "🔧 Building with custom version: $input_version"
|
||||
echo "⚠️ Warning: Custom version format may not follow standard patterns"
|
||||
# Invalid version for Docker build
|
||||
should_build=false
|
||||
echo "❌ Invalid version for Docker build: $input_version"
|
||||
echo "⚠️ Only release versions (latest, v1.0.0) and prereleases (v1.0.0-alpha1) are supported"
|
||||
;;
|
||||
esac
|
||||
elif [[ "${{ startsWith(github.ref, 'refs/tags/') }}" == "true" ]]; then
|
||||
# Tag push - release or prerelease
|
||||
tag_name="${GITHUB_REF#refs/tags/}"
|
||||
version="${tag_name}"
|
||||
|
||||
# Check if this is a prerelease
|
||||
if [[ "$tag_name" == *"alpha"* ]] || [[ "$tag_name" == *"beta"* ]] || [[ "$tag_name" == *"rc"* ]]; then
|
||||
build_type="prerelease"
|
||||
is_prerelease=true
|
||||
echo "🚀 Docker prerelease build detected: $tag_name"
|
||||
else
|
||||
build_type="release"
|
||||
create_latest=true
|
||||
echo "📦 Docker release build detected: $tag_name"
|
||||
fi
|
||||
elif [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
|
||||
# Main branch push - development build
|
||||
build_type="development"
|
||||
version="dev-${short_sha}"
|
||||
echo "🛠️ Docker development build detected"
|
||||
else
|
||||
# Other branches - development build
|
||||
build_type="development"
|
||||
version="dev-${short_sha}"
|
||||
echo "🔧 Docker development build detected"
|
||||
fi
|
||||
|
||||
# Push only on main branch, tags, or manual trigger
|
||||
if [[ "${{ github.ref }}" == "refs/heads/main" ]] || \
|
||||
[[ "${{ startsWith(github.ref, 'refs/tags/') }}" == "true" ]] || \
|
||||
[[ "${{ github.event.inputs.push_images }}" == "true" ]]; then
|
||||
should_push=true
|
||||
fi
|
||||
|
||||
echo "should_build=$should_build" >> $GITHUB_OUTPUT
|
||||
@@ -210,25 +206,15 @@ jobs:
|
||||
echo " - Create latest: $create_latest"
|
||||
|
||||
# Build multi-arch Docker images
|
||||
# Strategy: Build images using pre-built binaries from dl.rustfs.com
|
||||
# Supports both release and dev channel binaries based on build context
|
||||
# Only runs when should_build is true (which includes workflow success check)
|
||||
build-docker:
|
||||
name: Build Docker Images
|
||||
needs: build-check
|
||||
if: needs.build-check.outputs.should_build == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
variant:
|
||||
- name: production
|
||||
dockerfile: Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
- name: source
|
||||
dockerfile: Dockerfile.source
|
||||
platforms: linux/amd64,linux/arm64
|
||||
- name: dev
|
||||
dockerfile: Dockerfile.source
|
||||
platforms: linux/amd64,linux/arm64
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
@@ -236,9 +222,8 @@ jobs:
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
username: ${{ env.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
scopes: repository:rustfs/rustfs:pull,push
|
||||
|
||||
# - name: Login to GitHub Container Registry
|
||||
# uses: docker/login-action@v3
|
||||
@@ -260,57 +245,54 @@ jobs:
|
||||
VERSION="${{ needs.build-check.outputs.version }}"
|
||||
SHORT_SHA="${{ needs.build-check.outputs.short_sha }}"
|
||||
CREATE_LATEST="${{ needs.build-check.outputs.create_latest }}"
|
||||
VARIANT="${{ matrix.variant.name }}"
|
||||
|
||||
# Convert version format for Dockerfile compatibility
|
||||
case "$VERSION" in
|
||||
"latest")
|
||||
# For stable latest, use RELEASE=latest + release CHANNEL
|
||||
DOCKER_RELEASE="latest"
|
||||
DOCKER_CHANNEL="release"
|
||||
;;
|
||||
v*)
|
||||
# For versioned releases (v1.0.0), remove 'v' prefix for Dockerfile
|
||||
DOCKER_RELEASE="${VERSION#v}"
|
||||
DOCKER_CHANNEL="release"
|
||||
;;
|
||||
*)
|
||||
# For other versions, pass as-is
|
||||
DOCKER_RELEASE="${VERSION}"
|
||||
DOCKER_CHANNEL="release"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "docker_release=$DOCKER_RELEASE" >> $GITHUB_OUTPUT
|
||||
echo "docker_channel=$DOCKER_CHANNEL" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "🐳 Docker build parameters:"
|
||||
echo " - Original version: $VERSION"
|
||||
echo " - Docker RELEASE: $DOCKER_RELEASE"
|
||||
echo " - Docker CHANNEL: $DOCKER_CHANNEL"
|
||||
|
||||
# Generate tags based on build type
|
||||
TAGS=""
|
||||
# Only support release and prerelease builds (no development builds)
|
||||
TAGS="${{ env.REGISTRY_DOCKERHUB }}:${VERSION}"
|
||||
|
||||
if [[ "$BUILD_TYPE" == "development" ]]; then
|
||||
# Development build: dev-${short_sha}-${variant} and dev-${variant}
|
||||
TAGS="${{ env.REGISTRY_DOCKERHUB }}:dev-${SHORT_SHA}-${VARIANT}"
|
||||
|
||||
# Add rolling dev tag for each variant
|
||||
TAGS="$TAGS,${{ env.REGISTRY_DOCKERHUB }}:dev-${VARIANT}"
|
||||
|
||||
# Special handling for production variant
|
||||
if [[ "$VARIANT" == "production" ]]; then
|
||||
TAGS="$TAGS,${{ env.REGISTRY_DOCKERHUB }}:dev-${SHORT_SHA}"
|
||||
TAGS="$TAGS,${{ env.REGISTRY_DOCKERHUB }}:dev"
|
||||
fi
|
||||
else
|
||||
# Release/Prerelease build: ${version}-${variant}
|
||||
TAGS="${{ env.REGISTRY_DOCKERHUB }}:${VERSION}-${VARIANT}"
|
||||
|
||||
# Special handling for production variant - create main version tag
|
||||
if [[ "$VARIANT" == "production" ]]; then
|
||||
TAGS="$TAGS,${{ env.REGISTRY_DOCKERHUB }}:${VERSION}"
|
||||
# Add channel tags for prereleases and latest for stable
|
||||
if [[ "$CREATE_LATEST" == "true" ]]; then
|
||||
# Stable release
|
||||
TAGS="$TAGS,${{ env.REGISTRY_DOCKERHUB }}:latest"
|
||||
elif [[ "$BUILD_TYPE" == "prerelease" ]]; then
|
||||
# Prerelease channel tags (alpha, beta, rc)
|
||||
if [[ "$VERSION" == *"alpha"* ]]; then
|
||||
CHANNEL="alpha"
|
||||
elif [[ "$VERSION" == *"beta"* ]]; then
|
||||
CHANNEL="beta"
|
||||
elif [[ "$VERSION" == *"rc"* ]]; then
|
||||
CHANNEL="rc"
|
||||
fi
|
||||
|
||||
# Add channel tags for prereleases and latest for stable
|
||||
if [[ "$CREATE_LATEST" == "true" ]]; then
|
||||
# Stable release
|
||||
if [[ "$VARIANT" == "production" ]]; then
|
||||
TAGS="$TAGS,${{ env.REGISTRY_DOCKERHUB }}:latest"
|
||||
else
|
||||
TAGS="$TAGS,${{ env.REGISTRY_DOCKERHUB }}:latest-${VARIANT}"
|
||||
fi
|
||||
elif [[ "$BUILD_TYPE" == "prerelease" ]]; then
|
||||
# Prerelease channel tags (alpha, beta, rc)
|
||||
if [[ "$VERSION" == *"alpha"* ]]; then
|
||||
CHANNEL="alpha"
|
||||
elif [[ "$VERSION" == *"beta"* ]]; then
|
||||
CHANNEL="beta"
|
||||
elif [[ "$VERSION" == *"rc"* ]]; then
|
||||
CHANNEL="rc"
|
||||
fi
|
||||
|
||||
if [[ -n "$CHANNEL" ]]; then
|
||||
if [[ "$VARIANT" == "production" ]]; then
|
||||
TAGS="$TAGS,${{ env.REGISTRY_DOCKERHUB }}:${CHANNEL}"
|
||||
else
|
||||
TAGS="$TAGS,${{ env.REGISTRY_DOCKERHUB }}:${CHANNEL}-${VARIANT}"
|
||||
fi
|
||||
fi
|
||||
if [[ -n "$CHANNEL" ]]; then
|
||||
TAGS="$TAGS,${{ env.REGISTRY_DOCKERHUB }}:${CHANNEL}"
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -324,7 +306,6 @@ jobs:
|
||||
LABELS="$LABELS,org.opencontainers.image.revision=${{ github.sha }}"
|
||||
LABELS="$LABELS,org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}"
|
||||
LABELS="$LABELS,org.opencontainers.image.created=$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
|
||||
LABELS="$LABELS,org.opencontainers.image.variant=$VARIANT"
|
||||
LABELS="$LABELS,org.opencontainers.image.build-type=$BUILD_TYPE"
|
||||
|
||||
echo "labels=$LABELS" >> $GITHUB_OUTPUT
|
||||
@@ -338,20 +319,22 @@ jobs:
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ${{ matrix.variant.dockerfile }}
|
||||
platforms: ${{ matrix.variant.platforms }}
|
||||
file: Dockerfile
|
||||
platforms: ${{ env.DOCKER_PLATFORMS }}
|
||||
push: ${{ needs.build-check.outputs.should_push == 'true' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: |
|
||||
type=gha,scope=docker-${{ matrix.variant.name }}
|
||||
type=gha,scope=docker-binary
|
||||
cache-to: |
|
||||
type=gha,mode=max,scope=docker-${{ matrix.variant.name }}
|
||||
type=gha,mode=max,scope=docker-binary
|
||||
build-args: |
|
||||
BUILDTIME=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
|
||||
VERSION=${{ needs.build-check.outputs.version }}
|
||||
BUILD_TYPE=${{ needs.build-check.outputs.build_type }}
|
||||
REVISION=${{ github.sha }}
|
||||
RELEASE=${{ steps.meta.outputs.docker_release }}
|
||||
CHANNEL=${{ steps.meta.outputs.docker_channel }}
|
||||
BUILDKIT_INLINE_CACHE=1
|
||||
# Enable advanced BuildKit features for better performance
|
||||
provenance: false
|
||||
@@ -360,38 +343,8 @@ jobs:
|
||||
no-cache: false
|
||||
pull: true
|
||||
|
||||
# Create manifest for main production image (only for stable releases)
|
||||
create-manifest:
|
||||
name: Create Manifest
|
||||
needs: [build-check, build-docker]
|
||||
if: needs.build-check.outputs.should_push == 'true' && needs.build-check.outputs.create_latest == 'true' && needs.build-check.outputs.build_type == 'release'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
# - name: Login to GitHub Container Registry
|
||||
# uses: docker/login-action@v3
|
||||
# with:
|
||||
# registry: ghcr.io
|
||||
# username: ${{ github.actor }}
|
||||
# password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create and push manifest
|
||||
run: |
|
||||
VERSION="${{ needs.build-check.outputs.version }}"
|
||||
|
||||
echo "🐳 Creating manifest for stable release: $VERSION"
|
||||
|
||||
# Create main image tag (without variant suffix) for stable releases only
|
||||
# Note: The "production" variant already creates the main tags without suffix
|
||||
echo "Manifest creation is handled by the production variant build step"
|
||||
echo "Main tags ${VERSION} and latest are created directly by the production variant"
|
||||
|
||||
echo "✅ Manifest created successfully for stable release"
|
||||
# Note: Manifest creation is no longer needed as we only build one variant
|
||||
# Multi-arch manifests are automatically created by docker/build-push-action
|
||||
|
||||
# Docker build summary
|
||||
docker-summary:
|
||||
@@ -409,23 +362,23 @@ jobs:
|
||||
echo "🐳 Docker build completed successfully!"
|
||||
echo "📦 Build type: $BUILD_TYPE"
|
||||
echo "🔢 Version: $VERSION"
|
||||
echo "🚀 Strategy: Images using pre-built binaries (release channel only)"
|
||||
echo ""
|
||||
|
||||
case "$BUILD_TYPE" in
|
||||
"development")
|
||||
echo "🛠️ Development Docker images have been built with dev-${VERSION} tags"
|
||||
echo "⚠️ These are development images - not suitable for production use"
|
||||
;;
|
||||
"release")
|
||||
echo "🚀 Release Docker images have been built with v${VERSION} tags"
|
||||
echo "✅ These images are ready for production use"
|
||||
echo "🚀 Release Docker image has been built with ${VERSION} tags"
|
||||
echo "✅ This image is ready for production use"
|
||||
if [[ "$CREATE_LATEST" == "true" ]]; then
|
||||
echo "🏷️ Latest tags have been created for stable release"
|
||||
echo "🏷️ Latest tag has been created for stable release"
|
||||
fi
|
||||
;;
|
||||
"prerelease")
|
||||
echo "🧪 Prerelease Docker images have been built with v${VERSION} tags"
|
||||
echo "⚠️ These are prerelease images - use with caution"
|
||||
echo "🚫 Latest tags NOT created for prerelease"
|
||||
echo "🧪 Prerelease Docker image has been built with ${VERSION} tags"
|
||||
echo "⚠️ This is a prerelease image - use with caution"
|
||||
echo "🚫 Latest tag NOT created for prerelease"
|
||||
;;
|
||||
*)
|
||||
echo "❌ Unexpected build type: $BUILD_TYPE"
|
||||
;;
|
||||
esac
|
||||
|
||||
45
Dockerfile
45
Dockerfile
@@ -1,10 +1,10 @@
|
||||
# Multi-stage build for RustFS production image
|
||||
FROM alpine:latest AS build
|
||||
|
||||
# Build arguments
|
||||
ARG TARGETARCH
|
||||
# Build arguments - use TARGETPLATFORM for consistency with Dockerfile.source
|
||||
ARG TARGETPLATFORM
|
||||
ARG BUILDPLATFORM
|
||||
ARG RELEASE=latest
|
||||
ARG CHANNEL=release
|
||||
|
||||
# Install dependencies for downloading and verifying binaries
|
||||
RUN apk add --no-cache \
|
||||
@@ -18,34 +18,28 @@ RUN apk add --no-cache \
|
||||
# Create build directory
|
||||
WORKDIR /build
|
||||
|
||||
# Map TARGETARCH to architecture format used in builds
|
||||
RUN case "${TARGETARCH}" in \
|
||||
"amd64") ARCH="x86_64" ;; \
|
||||
"arm64") ARCH="aarch64" ;; \
|
||||
*) echo "Unsupported architecture: ${TARGETARCH}" && exit 1 ;; \
|
||||
# Map TARGETPLATFORM to architecture format used in builds
|
||||
RUN case "${TARGETPLATFORM}" in \
|
||||
"linux/amd64") ARCH="x86_64" ;; \
|
||||
"linux/arm64") ARCH="aarch64" ;; \
|
||||
*) echo "Unsupported platform: ${TARGETPLATFORM}" && exit 1 ;; \
|
||||
esac && \
|
||||
echo "ARCH=${ARCH}" > /build/arch.env
|
||||
|
||||
# Download rustfs binary from dl.rustfs.com
|
||||
# Download rustfs binary from dl.rustfs.com (release channel only)
|
||||
RUN . /build/arch.env && \
|
||||
BASE_URL="https://dl.rustfs.com/artifacts/rustfs" && \
|
||||
BASE_URL="https://dl.rustfs.com/artifacts/rustfs/release" && \
|
||||
PLATFORM="linux" && \
|
||||
if [ "${RELEASE}" = "latest" ]; then \
|
||||
# Download latest version from specified channel \
|
||||
if [ "${CHANNEL}" = "dev" ]; then \
|
||||
PACKAGE_NAME="rustfs-${PLATFORM}-${ARCH}-dev-latest.zip"; \
|
||||
DOWNLOAD_URL="${BASE_URL}/dev/${PACKAGE_NAME}"; \
|
||||
echo "📥 Downloading latest dev build: ${PACKAGE_NAME}"; \
|
||||
else \
|
||||
PACKAGE_NAME="rustfs-${PLATFORM}-${ARCH}-latest.zip"; \
|
||||
DOWNLOAD_URL="${BASE_URL}/release/${PACKAGE_NAME}"; \
|
||||
echo "📥 Downloading latest release build: ${PACKAGE_NAME}"; \
|
||||
fi; \
|
||||
# Download latest release version \
|
||||
PACKAGE_NAME="rustfs-${PLATFORM}-${ARCH}-latest.zip"; \
|
||||
DOWNLOAD_URL="${BASE_URL}/${PACKAGE_NAME}"; \
|
||||
echo "📥 Downloading latest release build: ${PACKAGE_NAME}"; \
|
||||
else \
|
||||
# Download specific version (always from release channel) \
|
||||
# Download specific release version \
|
||||
PACKAGE_NAME="rustfs-${PLATFORM}-${ARCH}-v${RELEASE}.zip"; \
|
||||
DOWNLOAD_URL="${BASE_URL}/release/${PACKAGE_NAME}"; \
|
||||
echo "📥 Downloading specific version: ${PACKAGE_NAME}"; \
|
||||
DOWNLOAD_URL="${BASE_URL}/${PACKAGE_NAME}"; \
|
||||
echo "📥 Downloading specific release version: ${PACKAGE_NAME}"; \
|
||||
fi && \
|
||||
echo "🔗 Download URL: ${DOWNLOAD_URL}" && \
|
||||
curl -f -L "${DOWNLOAD_URL}" -o /build/rustfs.zip && \
|
||||
@@ -65,7 +59,6 @@ FROM alpine:latest
|
||||
|
||||
# Set build arguments and labels
|
||||
ARG RELEASE=latest
|
||||
ARG CHANNEL=release
|
||||
ARG BUILD_DATE
|
||||
ARG VCS_REF
|
||||
|
||||
@@ -74,7 +67,6 @@ LABEL name="RustFS" \
|
||||
maintainer="RustFS Team <dev@rustfs.com>" \
|
||||
version="${RELEASE}" \
|
||||
release="${RELEASE}" \
|
||||
channel="${CHANNEL}" \
|
||||
build-date="${BUILD_DATE}" \
|
||||
vcs-ref="${VCS_REF}" \
|
||||
summary="RustFS is a high-performance distributed object storage system written in Rust, compatible with S3 API." \
|
||||
@@ -121,9 +113,6 @@ WORKDIR /data
|
||||
# Expose port
|
||||
EXPOSE 9000
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --retries=3 --start-period=40s \
|
||||
CMD curl -f http://localhost:9000/health || exit 1
|
||||
|
||||
# Volume for data
|
||||
VOLUME ["/data"]
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
# Multi-stage Dockerfile for RustFS
|
||||
# Multi-stage Dockerfile for RustFS - LOCAL DEVELOPMENT ONLY
|
||||
#
|
||||
# ⚠️ IMPORTANT: This Dockerfile is for local development and testing only.
|
||||
# ⚠️ It builds RustFS from source code and is NOT used in CI/CD pipelines.
|
||||
# ⚠️ CI/CD pipeline uses pre-built binaries from Dockerfile instead.
|
||||
#
|
||||
# Usage for local development:
|
||||
# docker build -f Dockerfile.source -t rustfs:dev-local .
|
||||
# docker run --rm -p 9000:9000 rustfs:dev-local
|
||||
#
|
||||
# Supports cross-compilation for amd64 and arm64 architectures
|
||||
ARG TARGETPLATFORM
|
||||
ARG BUILDPLATFORM
|
||||
@@ -6,6 +15,13 @@ ARG BUILDPLATFORM
|
||||
# Build stage
|
||||
FROM --platform=$BUILDPLATFORM rust:1.88-bookworm AS builder
|
||||
|
||||
# Re-declare build arguments after FROM (required for multi-stage builds)
|
||||
ARG TARGETPLATFORM
|
||||
ARG BUILDPLATFORM
|
||||
|
||||
# Debug: Print platform information
|
||||
RUN echo "🐳 Build Info: BUILDPLATFORM=$BUILDPLATFORM, TARGETPLATFORM=$TARGETPLATFORM"
|
||||
|
||||
# Install required build dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
wget \
|
||||
@@ -18,17 +34,7 @@ RUN apt-get update && apt-get install -y \
|
||||
lld \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install sccache for Rust compilation caching
|
||||
RUN wget https://github.com/mozilla/sccache/releases/download/v0.8.1/sccache-v0.8.1-x86_64-unknown-linux-gnu.tar.gz \
|
||||
&& tar -xzf sccache-v0.8.1-x86_64-unknown-linux-gnu.tar.gz \
|
||||
&& mv sccache-v0.8.1-x86_64-unknown-linux-gnu/sccache /usr/local/bin/ \
|
||||
&& chmod +x /usr/local/bin/sccache \
|
||||
&& rm -rf sccache-v0.8.1-x86_64-unknown-linux-gnu.tar.gz sccache-v0.8.1-x86_64-unknown-linux-gnu
|
||||
|
||||
# Set up sccache environment
|
||||
ENV RUSTC_WRAPPER=sccache \
|
||||
SCCACHE_DIR=/tmp/sccache \
|
||||
SCCACHE_CACHE_SIZE=2G
|
||||
# Note: sccache removed for simpler builds
|
||||
|
||||
# Install cross-compilation tools for ARM64
|
||||
RUN if [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
|
||||
@@ -49,10 +55,13 @@ RUN wget https://github.com/google/flatbuffers/releases/download/v25.2.10/Linux.
|
||||
&& mv flatc /usr/local/bin/ && chmod +x /usr/local/bin/flatc && rm -rf Linux.flatc.binary.g++-13.zip
|
||||
|
||||
# Set up Rust targets based on platform
|
||||
RUN case "$TARGETPLATFORM" in \
|
||||
RUN set -e && \
|
||||
PLATFORM="${TARGETPLATFORM:-linux/amd64}" && \
|
||||
echo "🎯 Setting up Rust target for platform: $PLATFORM" && \
|
||||
case "$PLATFORM" in \
|
||||
"linux/amd64") rustup target add x86_64-unknown-linux-gnu ;; \
|
||||
"linux/arm64") rustup target add aarch64-unknown-linux-gnu ;; \
|
||||
*) echo "Unsupported platform: $TARGETPLATFORM" && exit 1 ;; \
|
||||
*) echo "❌ Unsupported platform: $PLATFORM" && exit 1 ;; \
|
||||
esac
|
||||
|
||||
# Set up environment for cross-compilation
|
||||
@@ -62,17 +71,8 @@ ENV CXX_aarch64_unknown_linux_gnu=aarch64-linux-gnu-g++
|
||||
|
||||
WORKDIR /usr/src/rustfs
|
||||
|
||||
# Copy cargo configuration for optimized builds
|
||||
COPY cargo.config.toml ./.cargo/config.toml
|
||||
|
||||
# Copy Cargo files for dependency caching
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY */Cargo.toml ./*/
|
||||
|
||||
# Create dummy main.rs files for dependency compilation
|
||||
RUN find . -name "Cargo.toml" -not -path "./Cargo.toml" | \
|
||||
xargs -I {} dirname {} | \
|
||||
xargs -I {} sh -c 'mkdir -p {}/src && echo "fn main() {}" > {}/src/main.rs'
|
||||
# Copy all source code
|
||||
COPY . .
|
||||
|
||||
# Configure cargo for optimized builds
|
||||
ENV CARGO_NET_GIT_FETCH_WITH_CLI=true \
|
||||
@@ -82,32 +82,27 @@ ENV CARGO_NET_GIT_FETCH_WITH_CLI=true \
|
||||
CARGO_PROFILE_RELEASE_SPLIT_DEBUGINFO=off \
|
||||
CARGO_PROFILE_RELEASE_STRIP=symbols
|
||||
|
||||
# Build dependencies only (cache layer) with optimizations
|
||||
RUN sccache --start-server 2>/dev/null || true && \
|
||||
case "$TARGETPLATFORM" in \
|
||||
"linux/amd64") cargo build --release --target x86_64-unknown-linux-gnu -j $(nproc) ;; \
|
||||
"linux/arm64") cargo build --release --target aarch64-unknown-linux-gnu -j $(nproc) ;; \
|
||||
esac
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Generate protobuf code
|
||||
RUN cargo run --bin gproto
|
||||
|
||||
# Build the actual application with optimizations
|
||||
RUN sccache --start-server 2>/dev/null || true && \
|
||||
case "$TARGETPLATFORM" in \
|
||||
RUN case "$TARGETPLATFORM" in \
|
||||
"linux/amd64") \
|
||||
echo "🔨 Building for amd64..." && \
|
||||
rustup target add x86_64-unknown-linux-gnu && \
|
||||
cargo build --release --target x86_64-unknown-linux-gnu --bin rustfs -j $(nproc) && \
|
||||
cp target/x86_64-unknown-linux-gnu/release/rustfs /usr/local/bin/rustfs \
|
||||
;; \
|
||||
"linux/arm64") \
|
||||
echo "🔨 Building for arm64..." && \
|
||||
rustup target add aarch64-unknown-linux-gnu && \
|
||||
cargo build --release --target aarch64-unknown-linux-gnu --bin rustfs -j $(nproc) && \
|
||||
cp target/aarch64-unknown-linux-gnu/release/rustfs /usr/local/bin/rustfs \
|
||||
;; \
|
||||
esac && \
|
||||
sccache --show-stats || true
|
||||
*) \
|
||||
echo "❌ Unsupported platform: $TARGETPLATFORM" && exit 1 \
|
||||
;; \
|
||||
esac
|
||||
|
||||
# Runtime stage - Ubuntu minimal for better compatibility
|
||||
FROM ubuntu:22.04
|
||||
@@ -147,9 +142,6 @@ ENV RUSTFS_ACCESS_KEY=rustfsadmin \
|
||||
RUSTFS_VOLUMES=/data \
|
||||
RUST_LOG=warn
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:9000/health || exit 1
|
||||
|
||||
# Volume for data
|
||||
VOLUME ["/data"]
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
use crate::error::{Error, Result, is_err_config_not_found};
|
||||
use crate::sys::get_claims_from_token_with_secret;
|
||||
use crate::{
|
||||
cache::{Cache, CacheEntity},
|
||||
error::{Error as IamError, is_err_no_such_group, is_err_no_such_policy, is_err_no_such_user},
|
||||
@@ -26,7 +27,7 @@ use rustfs_ecstore::global::get_global_action_cred;
|
||||
use rustfs_madmin::{AccountStatus, AddOrUpdateUserReq, GroupDesc};
|
||||
use rustfs_policy::{
|
||||
arn::ARN,
|
||||
auth::{self, Credentials, UserIdentity, get_claims_from_token_with_secret, is_secret_key_valid, jwt_sign},
|
||||
auth::{self, Credentials, UserIdentity, is_secret_key_valid, jwt_sign},
|
||||
format::Format,
|
||||
policy::{
|
||||
EMBEDDED_POLICY_TYPE, INHERITED_POLICY_TYPE, Policy, PolicyDoc, default::DEFAULT_POLICIES, iam_policy_claim_name_sa,
|
||||
|
||||
@@ -23,6 +23,7 @@ use crate::store::GroupInfo;
|
||||
use crate::store::MappedPolicy;
|
||||
use crate::store::Store;
|
||||
use crate::store::UserType;
|
||||
use crate::utils::extract_claims;
|
||||
use rustfs_ecstore::global::get_global_action_cred;
|
||||
use rustfs_madmin::AddOrUpdateUserReq;
|
||||
use rustfs_madmin::GroupDesc;
|
||||
@@ -542,7 +543,7 @@ impl<T: Store> IamSys<T> {
|
||||
}
|
||||
};
|
||||
|
||||
if policies.is_empty() {
|
||||
if !is_owner && policies.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -732,3 +733,18 @@ pub struct UpdateServiceAccountOpts {
|
||||
pub expiration: Option<OffsetDateTime>,
|
||||
pub status: Option<String>,
|
||||
}
|
||||
|
||||
pub fn get_claims_from_token_with_secret(token: &str, secret: &str) -> Result<HashMap<String, Value>> {
|
||||
let mut ms =
|
||||
extract_claims::<HashMap<String, Value>>(token, secret).map_err(|e| Error::other(format!("extract claims err {e}")))?;
|
||||
|
||||
if let Some(session_policy) = ms.claims.get(SESSION_POLICY_NAME) {
|
||||
let policy_str = session_policy.as_str().unwrap_or_default();
|
||||
let policy = base64_decode(policy_str.as_bytes()).map_err(|e| Error::other(format!("base64 decode err {e}")))?;
|
||||
ms.claims.insert(
|
||||
SESSION_POLICY_NAME_EXTRACTED.to_string(),
|
||||
Value::String(String::from_utf8(policy).map_err(|e| Error::other(format!("utf8 decode err {e}")))?),
|
||||
);
|
||||
}
|
||||
Ok(ms.claims)
|
||||
}
|
||||
|
||||
@@ -16,8 +16,6 @@ use crate::error::Error as IamError;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::policy::{INHERITED_POLICY_TYPE, Policy, Validator, iam_policy_claim_name_sa};
|
||||
use crate::utils;
|
||||
use crate::utils::extract_claims;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Value, json};
|
||||
use std::collections::HashMap;
|
||||
@@ -253,12 +251,6 @@ pub fn create_new_credentials_with_metadata(
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_claims_from_token_with_secret<T: DeserializeOwned>(token: &str, secret: &str) -> Result<T> {
|
||||
let ms = extract_claims::<T>(token, secret)?;
|
||||
// TODO SessionPolicyName
|
||||
Ok(ms.claims)
|
||||
}
|
||||
|
||||
pub fn jwt_sign<T: Serialize>(claims: &T, token_secret: &str) -> Result<String> {
|
||||
let token = utils::generate_jwt(claims, token_secret)?;
|
||||
Ok(token)
|
||||
|
||||
@@ -248,6 +248,8 @@ done
|
||||
# Main execution
|
||||
main() {
|
||||
print_message $BLUE "🐳 RustFS Docker Buildx Build Script"
|
||||
print_message $YELLOW "📋 Build Strategy: Uses pre-built binaries from dl.rustfs.com"
|
||||
print_message $YELLOW "🚀 Production images only - optimized for distribution"
|
||||
echo ""
|
||||
|
||||
# Check prerequisites
|
||||
|
||||
@@ -17,8 +17,8 @@ use http::Uri;
|
||||
use rustfs_ecstore::global::get_global_action_cred;
|
||||
use rustfs_iam::error::Error as IamError;
|
||||
use rustfs_iam::sys::SESSION_POLICY_NAME;
|
||||
use rustfs_iam::sys::get_claims_from_token_with_secret;
|
||||
use rustfs_policy::auth;
|
||||
use rustfs_policy::auth::get_claims_from_token_with_secret;
|
||||
use s3s::S3Error;
|
||||
use s3s::S3ErrorCode;
|
||||
use s3s::S3Result;
|
||||
|
||||
@@ -199,7 +199,7 @@ async fn run(opt: config::Opt) -> Result<()> {
|
||||
if result.update_available {
|
||||
if let Some(latest) = &result.latest_version {
|
||||
info!(
|
||||
"🚀 New version available: {} -> {} (current: {})",
|
||||
"🚀 Version check: New version available: {} -> {} (current: {})",
|
||||
result.current_version, latest.version, result.current_version
|
||||
);
|
||||
if let Some(notes) = &latest.release_notes {
|
||||
@@ -210,14 +210,14 @@ async fn run(opt: config::Opt) -> Result<()> {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debug!("✅ Current version is up to date: {}", result.current_version);
|
||||
debug!("✅ Version check: Current version is up to date: {}", result.current_version);
|
||||
}
|
||||
}
|
||||
Err(UpdateCheckError::HttpError(e)) => {
|
||||
debug!("Version check network error (this is normal): {}", e);
|
||||
debug!("Version check: network error (this is normal): {}", e);
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("Version check failed (this is normal): {}", e);
|
||||
debug!("Version check: failed (this is normal): {}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
use thiserror::Error;
|
||||
use tracing::{debug, error, info, warn};
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
use crate::version;
|
||||
|
||||
@@ -86,7 +86,7 @@ impl VersionChecker {
|
||||
|
||||
Self {
|
||||
client,
|
||||
version_url: "https://version.rustfs.com".to_string(),
|
||||
version_url: "https://version.rustfs.com/latest.json".to_string(),
|
||||
timeout: Duration::from_secs(10),
|
||||
}
|
||||
}
|
||||
@@ -139,8 +139,9 @@ impl VersionChecker {
|
||||
|
||||
debug!("Retrieved latest version information: {:?}", version_info);
|
||||
|
||||
// Compare versions
|
||||
let update_available = self.is_newer_version(¤t_version, &version_info.version)?;
|
||||
// Compare versions using version.rs functions
|
||||
let update_available = version::is_newer_version(¤t_version, &version_info.version)
|
||||
.map_err(|e| UpdateCheckError::VersionParseError(e.to_string()))?;
|
||||
|
||||
let result = UpdateCheckResult {
|
||||
update_available,
|
||||
@@ -161,74 +162,6 @@ impl VersionChecker {
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Compare version numbers to determine if there's an update
|
||||
fn is_newer_version(&self, current: &str, latest: &str) -> Result<bool, UpdateCheckError> {
|
||||
// Clean version numbers, remove prefixes like "v", "RELEASE.", etc.
|
||||
let current_clean = self.clean_version(current);
|
||||
let latest_clean = self.clean_version(latest);
|
||||
|
||||
debug!("Version comparison: current='{}' vs latest='{}'", current_clean, latest_clean);
|
||||
|
||||
// If versions are the same, no update is needed
|
||||
if current_clean == latest_clean {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// Try semantic version comparison
|
||||
match self.compare_semantic_versions(¤t_clean, &latest_clean) {
|
||||
Ok(is_newer) => Ok(is_newer),
|
||||
Err(_) => {
|
||||
// If semantic version comparison fails, use string comparison
|
||||
warn!("Semantic version comparison failed, using string comparison");
|
||||
Ok(latest_clean > current_clean)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Clean version string
|
||||
fn clean_version(&self, version: &str) -> String {
|
||||
version
|
||||
.trim()
|
||||
.trim_start_matches('v')
|
||||
.trim_start_matches("RELEASE.")
|
||||
.trim_start_matches('@')
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// Semantic version comparison
|
||||
fn compare_semantic_versions(&self, current: &str, latest: &str) -> Result<bool, UpdateCheckError> {
|
||||
let current_parts = self.parse_version_parts(current)?;
|
||||
let latest_parts = self.parse_version_parts(latest)?;
|
||||
|
||||
// Use tuple comparison for lexicographic ordering
|
||||
Ok(latest_parts > current_parts)
|
||||
}
|
||||
|
||||
/// Parse version parts (major, minor, patch)
|
||||
fn parse_version_parts(&self, version: &str) -> Result<(u32, u32, u32), UpdateCheckError> {
|
||||
let parts: Vec<&str> = version.split('.').collect();
|
||||
|
||||
if parts.len() < 3 {
|
||||
return Err(UpdateCheckError::VersionParseError(format!("Invalid version format: {version}")));
|
||||
}
|
||||
|
||||
let major = parts[0]
|
||||
.parse::<u32>()
|
||||
.map_err(|_| UpdateCheckError::VersionParseError(format!("Cannot parse major version: {}", parts[0])))?;
|
||||
|
||||
let minor = parts[1]
|
||||
.parse::<u32>()
|
||||
.map_err(|_| UpdateCheckError::VersionParseError(format!("Cannot parse minor version: {}", parts[1])))?;
|
||||
|
||||
// Patch version may contain other characters, only take numeric part
|
||||
let patch_str = parts[2].chars().take_while(|c| c.is_numeric()).collect::<String>();
|
||||
let patch = patch_str
|
||||
.parse::<u32>()
|
||||
.map_err(|_| UpdateCheckError::VersionParseError(format!("Cannot parse patch version: {}", parts[2])))?;
|
||||
|
||||
Ok((major, minor, patch))
|
||||
}
|
||||
}
|
||||
|
||||
/// Get current version number
|
||||
@@ -253,37 +186,6 @@ pub async fn check_updates_with_url(url: String) -> Result<UpdateCheckResult, Up
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_clean_version() {
|
||||
let checker = VersionChecker::new();
|
||||
|
||||
assert_eq!(checker.clean_version("v1.0.0"), "1.0.0");
|
||||
assert_eq!(checker.clean_version("RELEASE.1.0.0"), "1.0.0");
|
||||
assert_eq!(checker.clean_version("@1.0.0"), "1.0.0");
|
||||
assert_eq!(checker.clean_version("1.0.0"), "1.0.0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_version_parts() {
|
||||
let checker = VersionChecker::new();
|
||||
|
||||
assert_eq!(checker.parse_version_parts("1.0.0").unwrap(), (1, 0, 0));
|
||||
assert_eq!(checker.parse_version_parts("2.1.3").unwrap(), (2, 1, 3));
|
||||
assert_eq!(checker.parse_version_parts("1.0.0-beta").unwrap(), (1, 0, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_version_comparison() {
|
||||
let checker = VersionChecker::new();
|
||||
|
||||
// Test semantic version comparison
|
||||
assert!(checker.is_newer_version("1.0.0", "1.0.1").unwrap());
|
||||
assert!(checker.is_newer_version("1.0.0", "1.1.0").unwrap());
|
||||
assert!(checker.is_newer_version("1.0.0", "2.0.0").unwrap());
|
||||
assert!(!checker.is_newer_version("1.0.1", "1.0.0").unwrap());
|
||||
assert!(!checker.is_newer_version("1.0.0", "1.0.0").unwrap());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_current_version() {
|
||||
let version = get_current_version();
|
||||
@@ -428,4 +330,21 @@ mod tests {
|
||||
|
||||
println!("✅ VersionInfo tests passed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_version_functions_integration() {
|
||||
// Test that version functions from version.rs work correctly
|
||||
assert_eq!(version::clean_version("refs/tags/1.0.0-alpha.17"), "1.0.0-alpha.17");
|
||||
assert_eq!(version::clean_version("v1.0.0"), "1.0.0");
|
||||
|
||||
// Test version comparison
|
||||
assert!(version::is_newer_version("1.0.0", "1.0.1").unwrap());
|
||||
assert!(!version::is_newer_version("1.0.1", "1.0.0").unwrap());
|
||||
|
||||
// Test version parsing using parse_version
|
||||
assert_eq!(version::parse_version("1.0.0").unwrap(), (1, 0, 0, None));
|
||||
assert_eq!(version::parse_version("2.1.3-alpha.1").unwrap(), (2, 1, 3, Some("alpha.1".to_string())));
|
||||
|
||||
println!("✅ Version functions integration tests passed");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,362 @@
|
||||
use shadow_rs::shadow;
|
||||
use std::process::Command;
|
||||
|
||||
shadow!(build);
|
||||
|
||||
type VersionParseResult = Result<(u32, u32, u32, Option<String>), Box<dyn std::error::Error>>;
|
||||
|
||||
#[allow(clippy::const_is_empty)]
|
||||
pub fn get_version() -> String {
|
||||
// 获取最新的 tag
|
||||
if let Ok(latest_tag) = get_latest_tag() {
|
||||
// 检查当前 commit 是否比最新 tag 更新
|
||||
if is_head_newer_than_tag(&latest_tag) {
|
||||
// 如果当前 commit 更新,则提升版本号
|
||||
if let Ok(new_version) = increment_version(&latest_tag) {
|
||||
return format!("refs/tags/{new_version}");
|
||||
}
|
||||
}
|
||||
|
||||
// 如果当前 commit 就是最新 tag,或者版本提升失败,返回当前 tag
|
||||
return format!("refs/tags/{latest_tag}");
|
||||
}
|
||||
|
||||
// 如果没有 tag,使用原来的逻辑
|
||||
if !build::TAG.is_empty() {
|
||||
build::TAG.to_string()
|
||||
format!("refs/tags/{}", build::TAG)
|
||||
} else if !build::SHORT_COMMIT.is_empty() {
|
||||
format!("@{}", build::SHORT_COMMIT)
|
||||
} else {
|
||||
build::PKG_VERSION.to_string()
|
||||
format!("refs/tags/{}", build::PKG_VERSION)
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取最新的 git tag
|
||||
fn get_latest_tag() -> Result<String, Box<dyn std::error::Error>> {
|
||||
let output = Command::new("git").args(["describe", "--tags", "--abbrev=0"]).output()?;
|
||||
|
||||
if output.status.success() {
|
||||
let tag = String::from_utf8(output.stdout)?;
|
||||
Ok(tag.trim().to_string())
|
||||
} else {
|
||||
Err("Failed to get latest tag".into())
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查当前 HEAD 是否比指定的 tag 更新
|
||||
fn is_head_newer_than_tag(tag: &str) -> bool {
|
||||
let output = Command::new("git")
|
||||
.args(["merge-base", "--is-ancestor", tag, "HEAD"])
|
||||
.output();
|
||||
|
||||
match output {
|
||||
Ok(result) => result.status.success(),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// 提升版本号(增加 patch 版本)
|
||||
fn increment_version(version: &str) -> Result<String, Box<dyn std::error::Error>> {
|
||||
// 解析版本号,例如 "1.0.0-alpha.19" -> (1, 0, 0, Some("alpha.19"))
|
||||
let (major, minor, patch, pre_release) = parse_version(version)?;
|
||||
|
||||
// 如果有预发布标识符,则增加预发布版本号
|
||||
if let Some(pre) = pre_release {
|
||||
if let Some(new_pre) = increment_pre_release(&pre) {
|
||||
return Ok(format!("{major}.{minor}.{patch}-{new_pre}"));
|
||||
}
|
||||
}
|
||||
|
||||
// 否则增加 patch 版本号
|
||||
Ok(format!("{major}.{minor}.{}", patch + 1))
|
||||
}
|
||||
|
||||
/// 解析版本号
|
||||
pub fn parse_version(version: &str) -> VersionParseResult {
|
||||
let parts: Vec<&str> = version.split('-').collect();
|
||||
let base_version = parts[0];
|
||||
let pre_release = if parts.len() > 1 { Some(parts[1..].join("-")) } else { None };
|
||||
|
||||
let version_parts: Vec<&str> = base_version.split('.').collect();
|
||||
if version_parts.len() < 3 {
|
||||
return Err("Invalid version format".into());
|
||||
}
|
||||
|
||||
let major: u32 = version_parts[0].parse()?;
|
||||
let minor: u32 = version_parts[1].parse()?;
|
||||
let patch: u32 = version_parts[2].parse()?;
|
||||
|
||||
Ok((major, minor, patch, pre_release))
|
||||
}
|
||||
|
||||
/// 增加预发布版本号
|
||||
fn increment_pre_release(pre_release: &str) -> Option<String> {
|
||||
// 处理形如 "alpha.19" 的预发布版本
|
||||
let parts: Vec<&str> = pre_release.split('.').collect();
|
||||
if parts.len() == 2 {
|
||||
if let Ok(num) = parts[1].parse::<u32>() {
|
||||
return Some(format!("{}.{}", parts[0], num + 1));
|
||||
}
|
||||
}
|
||||
|
||||
// 处理形如 "alpha19" 的预发布版本
|
||||
if let Some(pos) = pre_release.rfind(|c: char| c.is_alphabetic()) {
|
||||
let prefix = &pre_release[..=pos];
|
||||
let suffix = &pre_release[pos + 1..];
|
||||
if let Ok(num) = suffix.parse::<u32>() {
|
||||
return Some(format!("{prefix}{}", num + 1));
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Clean version string - removes common prefixes
|
||||
pub fn clean_version(version: &str) -> String {
|
||||
version
|
||||
.trim()
|
||||
.trim_start_matches("refs/tags/")
|
||||
.trim_start_matches('v')
|
||||
.trim_start_matches("RELEASE.")
|
||||
.trim_start_matches('@')
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// Compare two versions to determine if the latest is newer
|
||||
pub fn is_newer_version(current: &str, latest: &str) -> Result<bool, Box<dyn std::error::Error>> {
|
||||
// Clean version numbers, remove prefixes like "v", "RELEASE.", etc.
|
||||
let current_clean = clean_version(current);
|
||||
let latest_clean = clean_version(latest);
|
||||
|
||||
// If versions are the same, no update is needed
|
||||
if current_clean == latest_clean {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// Try semantic version comparison using parse_version
|
||||
match (parse_version(¤t_clean), parse_version(&latest_clean)) {
|
||||
(Ok(current_parts), Ok(latest_parts)) => Ok(compare_version_parts(¤t_parts, &latest_parts)),
|
||||
(Err(_), _) | (_, Err(_)) => {
|
||||
// If semantic version comparison fails, use string comparison
|
||||
Ok(latest_clean > current_clean)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Compare two version parts tuples (major, minor, patch, pre_release)
|
||||
fn compare_version_parts(current: &(u32, u32, u32, Option<String>), latest: &(u32, u32, u32, Option<String>)) -> bool {
|
||||
let (cur_major, cur_minor, cur_patch, cur_pre) = current;
|
||||
let (lat_major, lat_minor, lat_patch, lat_pre) = latest;
|
||||
|
||||
// Compare major version
|
||||
if lat_major != cur_major {
|
||||
return lat_major > cur_major;
|
||||
}
|
||||
|
||||
// Compare minor version
|
||||
if lat_minor != cur_minor {
|
||||
return lat_minor > cur_minor;
|
||||
}
|
||||
|
||||
// Compare patch version
|
||||
if lat_patch != cur_patch {
|
||||
return lat_patch > cur_patch;
|
||||
}
|
||||
|
||||
// Compare pre-release versions
|
||||
match (cur_pre, lat_pre) {
|
||||
(None, None) => false, // Same version
|
||||
(Some(_), None) => true, // Pre-release < release
|
||||
(None, Some(_)) => false, // Release > pre-release
|
||||
(Some(cur_pre), Some(lat_pre)) => {
|
||||
// Both are pre-release, compare them
|
||||
compare_pre_release(cur_pre, lat_pre)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Compare pre-release versions
|
||||
fn compare_pre_release(current: &str, latest: &str) -> bool {
|
||||
// Split by dots and compare each part
|
||||
let current_parts: Vec<&str> = current.split('.').collect();
|
||||
let latest_parts: Vec<&str> = latest.split('.').collect();
|
||||
|
||||
for (cur_part, lat_part) in current_parts.iter().zip(latest_parts.iter()) {
|
||||
// Try to parse as numbers first
|
||||
match (cur_part.parse::<u32>(), lat_part.parse::<u32>()) {
|
||||
(Ok(cur_num), Ok(lat_num)) => {
|
||||
if cur_num != lat_num {
|
||||
return lat_num > cur_num;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// If not numbers, compare as strings
|
||||
if cur_part != lat_part {
|
||||
return lat_part > cur_part;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If all compared parts are equal, longer version is newer
|
||||
latest_parts.len() > current_parts.len()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_version() {
|
||||
// 测试标准版本解析
|
||||
let (major, minor, patch, pre_release) = parse_version("1.0.0").unwrap();
|
||||
assert_eq!(major, 1);
|
||||
assert_eq!(minor, 0);
|
||||
assert_eq!(patch, 0);
|
||||
assert_eq!(pre_release, None);
|
||||
|
||||
// 测试预发布版本解析
|
||||
let (major, minor, patch, pre_release) = parse_version("1.0.0-alpha.19").unwrap();
|
||||
assert_eq!(major, 1);
|
||||
assert_eq!(minor, 0);
|
||||
assert_eq!(patch, 0);
|
||||
assert_eq!(pre_release, Some("alpha.19".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_increment_pre_release() {
|
||||
// 测试 alpha.19 -> alpha.20
|
||||
assert_eq!(increment_pre_release("alpha.19"), Some("alpha.20".to_string()));
|
||||
|
||||
// 测试 beta.5 -> beta.6
|
||||
assert_eq!(increment_pre_release("beta.5"), Some("beta.6".to_string()));
|
||||
|
||||
// 测试无法解析的情况
|
||||
assert_eq!(increment_pre_release("unknown"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_increment_version() {
|
||||
// 测试预发布版本递增
|
||||
assert_eq!(increment_version("1.0.0-alpha.19").unwrap(), "1.0.0-alpha.20");
|
||||
|
||||
// 测试标准版本递增
|
||||
assert_eq!(increment_version("1.0.0").unwrap(), "1.0.1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_version_format() {
|
||||
// 测试版本格式是否以 refs/tags/ 开头
|
||||
let version = get_version();
|
||||
assert!(version.starts_with("refs/tags/") || version.starts_with("@"));
|
||||
|
||||
// 如果是 refs/tags/ 格式,应该包含版本号
|
||||
if let Some(version_part) = version.strip_prefix("refs/tags/") {
|
||||
assert!(!version_part.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_current_version_output() {
|
||||
// 显示当前版本输出
|
||||
let version = get_version();
|
||||
println!("Current version: {version}");
|
||||
|
||||
// 验证版本格式
|
||||
assert!(version.starts_with("refs/tags/") || version.starts_with("@"));
|
||||
|
||||
// 如果是 refs/tags/ 格式,验证版本号不为空
|
||||
if let Some(version_part) = version.strip_prefix("refs/tags/") {
|
||||
assert!(!version_part.is_empty());
|
||||
println!("Version part: {version_part}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clean_version() {
|
||||
assert_eq!(clean_version("v1.0.0"), "1.0.0");
|
||||
assert_eq!(clean_version("RELEASE.1.0.0"), "1.0.0");
|
||||
assert_eq!(clean_version("@1.0.0"), "1.0.0");
|
||||
assert_eq!(clean_version("1.0.0"), "1.0.0");
|
||||
assert_eq!(clean_version("refs/tags/1.0.0-alpha.17"), "1.0.0-alpha.17");
|
||||
assert_eq!(clean_version("refs/tags/v1.0.0"), "1.0.0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_newer_version() {
|
||||
// Test semantic version comparison
|
||||
assert!(is_newer_version("1.0.0", "1.0.1").unwrap());
|
||||
assert!(is_newer_version("1.0.0", "1.1.0").unwrap());
|
||||
assert!(is_newer_version("1.0.0", "2.0.0").unwrap());
|
||||
assert!(!is_newer_version("1.0.1", "1.0.0").unwrap());
|
||||
assert!(!is_newer_version("1.0.0", "1.0.0").unwrap());
|
||||
|
||||
// Test version comparison with pre-release identifiers
|
||||
assert!(is_newer_version("1.0.0-alpha.1", "1.0.0-alpha.2").unwrap());
|
||||
assert!(is_newer_version("1.0.0-alpha.17", "1.0.1").unwrap());
|
||||
assert!(is_newer_version("refs/tags/1.0.0-alpha.16", "refs/tags/1.0.0-alpha.17").unwrap());
|
||||
assert!(!is_newer_version("refs/tags/1.0.0-alpha.17", "refs/tags/1.0.0-alpha.16").unwrap());
|
||||
|
||||
// Test pre-release vs release comparison
|
||||
assert!(is_newer_version("1.0.0-alpha.1", "1.0.0").unwrap());
|
||||
assert!(is_newer_version("1.0.0-beta.1", "1.0.0").unwrap());
|
||||
assert!(!is_newer_version("1.0.0", "1.0.0-alpha.1").unwrap());
|
||||
assert!(!is_newer_version("1.0.0", "1.0.0-beta.1").unwrap());
|
||||
|
||||
// Test pre-release version ordering
|
||||
assert!(is_newer_version("1.0.0-alpha.1", "1.0.0-alpha.2").unwrap());
|
||||
assert!(is_newer_version("1.0.0-alpha.19", "1.0.0-alpha.20").unwrap());
|
||||
assert!(is_newer_version("1.0.0-alpha.1", "1.0.0-beta.1").unwrap());
|
||||
assert!(is_newer_version("1.0.0-beta.1", "1.0.0-rc.1").unwrap());
|
||||
|
||||
// Test complex pre-release versions
|
||||
assert!(is_newer_version("1.0.0-alpha.1.2", "1.0.0-alpha.1.3").unwrap());
|
||||
assert!(is_newer_version("1.0.0-alpha.1", "1.0.0-alpha.1.1").unwrap());
|
||||
assert!(!is_newer_version("1.0.0-alpha.1.3", "1.0.0-alpha.1.2").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compare_version_parts() {
|
||||
// Test basic version comparison
|
||||
assert!(compare_version_parts(&(1, 0, 0, None), &(1, 0, 1, None)));
|
||||
assert!(compare_version_parts(&(1, 0, 0, None), &(1, 1, 0, None)));
|
||||
assert!(compare_version_parts(&(1, 0, 0, None), &(2, 0, 0, None)));
|
||||
assert!(!compare_version_parts(&(1, 0, 1, None), &(1, 0, 0, None)));
|
||||
|
||||
// Test pre-release vs release
|
||||
assert!(compare_version_parts(&(1, 0, 0, Some("alpha.1".to_string())), &(1, 0, 0, None)));
|
||||
assert!(!compare_version_parts(&(1, 0, 0, None), &(1, 0, 0, Some("alpha.1".to_string()))));
|
||||
|
||||
// Test pre-release comparison
|
||||
assert!(compare_version_parts(
|
||||
&(1, 0, 0, Some("alpha.1".to_string())),
|
||||
&(1, 0, 0, Some("alpha.2".to_string()))
|
||||
));
|
||||
assert!(compare_version_parts(
|
||||
&(1, 0, 0, Some("alpha.19".to_string())),
|
||||
&(1, 0, 0, Some("alpha.20".to_string()))
|
||||
));
|
||||
assert!(compare_version_parts(
|
||||
&(1, 0, 0, Some("alpha.1".to_string())),
|
||||
&(1, 0, 0, Some("beta.1".to_string()))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compare_pre_release() {
|
||||
// Test numeric pre-release comparison
|
||||
assert!(compare_pre_release("alpha.1", "alpha.2"));
|
||||
assert!(compare_pre_release("alpha.19", "alpha.20"));
|
||||
assert!(!compare_pre_release("alpha.2", "alpha.1"));
|
||||
|
||||
// Test string pre-release comparison
|
||||
assert!(compare_pre_release("alpha.1", "beta.1"));
|
||||
assert!(compare_pre_release("beta.1", "rc.1"));
|
||||
assert!(!compare_pre_release("beta.1", "alpha.1"));
|
||||
|
||||
// Test complex pre-release comparison
|
||||
assert!(compare_pre_release("alpha.1.2", "alpha.1.3"));
|
||||
assert!(compare_pre_release("alpha.1", "alpha.1.1"));
|
||||
assert!(!compare_pre_release("alpha.1.3", "alpha.1.2"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user