Compare commits

..

17 Commits

Author SHA1 Message Date
安正超
b4f87a4fee feat: disable Docker builds for development versions (#239)
* feat: disable Docker builds for development versions

- Remove dev-latest, main-latest, and dev-* version options from manual triggers
- Skip Docker builds for development versions in workflow_run events
- Only build Docker images for releases (v1.0.0) and prereleases (v1.0.0-alpha1)
- Simplify tags generation logic by removing development branch handling
- Update workflow documentation to reflect release-only Docker strategy

BREAKING CHANGE: Development Docker images are no longer built automatically

* feat: remove dev channel support from Dockerfile

- Remove CHANNEL build argument (no longer needed)
- Simplify download logic to only support release channel
- Remove dev-specific package download paths
- Update BASE_URL to point directly to release directory
- Remove channel label from Docker image metadata
- Streamline version handling (latest vs specific release)

This aligns with the workflow changes that disabled dev Docker builds.
2025-07-17 06:06:40 +08:00
安正超
ee5f94a2e2 fix: use consistent short SHA generation across workflows (#238)
- Replace manual cut -c1-7 with git rev-parse --short in docker.yml
- Ensures consistent short SHA length between build.yml and docker.yml
- Git automatically adjusts length for uniqueness, preventing conflicts
2025-07-17 05:48:30 +08:00
安正超
9c3cf554d3 fix: correct Docker build logic for dev version downloads (#237) 2025-07-17 05:36:15 +08:00
安正超
addbfa5487 fix: resolve Docker workflow manual build parameter issues (#236)
- Remove unsupported 'scopes' parameter from docker/login-action@v3
  * Fixes 'Unexpected input(s) scopes' error during Docker Hub login

- Add version format conversion for Dockerfile compatibility
  * main-latest/dev-latest → RELEASE=latest + CHANNEL=dev
  * latest → RELEASE=latest + CHANNEL=release
  * dev-* → RELEASE=dev-* + CHANNEL=dev
  * v* → RELEASE={version without v} + CHANNEL=release

- Fix Docker build parameter passing
  * Use converted docker_release and docker_channel values
  * Ensures correct binary download URLs in Dockerfile

Resolves manual Docker build failures reported in:
https://github.com/rustfs/rustfs/actions/runs/16330398463/job/46131302262
2025-07-17 05:21:06 +08:00
安正超
5eb461d7b7 refactor: remove redundant linux_builds_success logic in docker workflow (#235)
- Remove linux_builds_success output and related variables
- Simplify build-docker condition to only check should_build
- The should_build check already includes workflow success verification
- Reduce code complexity while maintaining the same functionality
2025-07-17 05:09:41 +08:00
安正超
1ea45afcd7 feat: Implement precise Docker build triggering using workflow_run event (#233)
* fix: correct YAML indentation error in docker workflow

- Fix incorrect indentation at line 237 in .github/workflows/docker.yml
- Step 'Extract metadata and generate tags' had 12 spaces instead of 6
- This was causing YAML syntax validation to fail

* fix: restore unified build-rustfs task with correct YAML syntax

- Revert complex job separation back to single build-rustfs task
- Maintain Linux and macOS builds in unified matrix
- Fix YAML indentation and syntax issues
- Docker builds will use only Linux binaries as designed in Dockerfile

* feat: implement precise Docker build triggering using workflow_run

- Use workflow_run event to trigger Docker builds independently
- Add precise Linux build status checking via GitHub API
- Only trigger Docker builds when both Linux architectures succeed
- Remove coupling between build.yml and docker.yml workflows
- Improve TARGETPLATFORM consistency in Dockerfile

This resolves the issue where Docker builds would trigger even if
Linux ARM64 builds failed, causing missing binary artifacts during
multi-architecture Docker image creation.
2025-07-17 04:51:08 +08:00
安正超
dbd86f6aee fix: correct YAML indentation error in docker workflow (#232)
- Fix incorrect indentation at line 237 in .github/workflows/docker.yml
- Step 'Extract metadata and generate tags' had 12 spaces instead of 6
- This was causing YAML syntax validation to fail
2025-07-17 04:28:31 +08:00
overtrue
af693f7b3f refactor: restructure Docker build pipeline to depend on binary builds
- Change docker.yml to use workflow_call triggered by build.yml
- Remove redundant force_build parameter from build.yml
- Simplify build_docker parameter (build implies push in CI/CD)
- Add proper dependency chain: build.yml -> docker.yml -> registry
- Update documentation to reflect new architecture
- Mark Dockerfile.source as local development only
2025-07-17 04:19:20 +08:00
安正超
3be5ee6445 fix: simplify Dockerfile.source and resolve build issues (#231)
- Remove complex dependency caching to fix workspace structure issues
- Remove sccache to eliminate rustc wrapper errors
- Ensure target installation in build step for cross-compilation
- Add debug output and error handling for unsupported platforms
- Use simple COPY . . approach for more reliable builds
2025-07-16 23:53:28 +08:00
overtrue
0acc8fe26a fix: docker build from source 2025-07-16 23:46:30 +08:00
overtrue
ecf40eb86c fix: docker build from source 2025-07-16 23:43:34 +08:00
overtrue
48ce7055f8 fix: remove dockerhub username 2025-07-16 22:35:14 +08:00
weisd
749f55d688 feat: enhance version function with automatic version increment (#227) 2025-07-16 18:09:43 +08:00
loverustfs
e5d17f5382 Disable Dockerfile.source 2025-07-16 18:03:09 +08:00
weisd
982cc66c74 fix: Refactor session policy handling and fix owner permission check (#226) 2025-07-16 16:40:51 +08:00
loverustfs
74bf4909c8 Modify docker source file 2025-07-15 23:17:39 +08:00
loverustfs
9c956b4445 Disable other docker mode 2025-07-15 22:10:00 +08:00
12 changed files with 641 additions and 400 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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"]

View File

@@ -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"]

View File

@@ -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,

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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

View File

@@ -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;

View File

@@ -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);
}
}
});

View File

@@ -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(&current_version, &version_info.version)?;
// Compare versions using version.rs functions
let update_available = version::is_newer_version(&current_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(&current_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");
}
}

View File

@@ -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(&current_clean), parse_version(&latest_clean)) {
(Ok(current_parts), Ok(latest_parts)) => Ok(compare_version_parts(&current_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"));
}
}