# Copyright 2024 RustFS Team # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 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 # # Platform scope: # - Pushes to main (development builds) build the Linux targets only โ€” they # feed the dev download channel and Docker dev images. Tags, the weekly # schedule and manual dispatch build the full platform matrix. # # Manual Parameters: # - build_docker: Build and push Docker images (default: true) # - platforms: Comma-separated platform IDs or 'all' (default: all) name: Build and Release on: push: tags: [ "*.*.*" ] branches: [ main ] paths-ignore: - "**.md" - "**.txt" - ".github/**" - "docs/**" - "deploy/**" - "scripts/dev_*.sh" - "LICENSE*" - "README*" - "**/*.png" - "**/*.jpg" - "**/*.svg" - ".gitignore" - ".dockerignore" schedule: - cron: "0 1 * * 0" # Weekly on Sunday 01:00 UTC (staggered after the ci.yml midnight cron) workflow_dispatch: inputs: build_docker: description: "Build and push Docker images after binary build" required: false default: true type: boolean platforms: description: "Comma-separated targets or 'all' (e.g. linux-x86_64-musl,macos-aarch64)" required: false default: "all" type: string permissions: contents: read concurrency: group: ${{ github.workflow }}-${{ github.event_name == 'push' && github.ref || github.run_id }} cancel-in-progress: ${{ github.event_name == 'push' }} env: CARGO_TERM_COLOR: always RUST_BACKTRACE: 1 # Optimize build performance CARGO_INCREMENTAL: 0 jobs: # Build strategy check - determine build type based on trigger build-check: name: Build Strategy Check runs-on: ubuntu-latest outputs: should_build: ${{ steps.check.outputs.should_build }} build_type: ${{ steps.check.outputs.build_type }} version: ${{ steps.check.outputs.version }} short_sha: ${{ steps.check.outputs.short_sha }} is_prerelease: ${{ steps.check.outputs.is_prerelease }} steps: - name: Checkout repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - name: Determine build strategy id: check run: | should_build=false build_type="none" version="" short_sha="" is_prerelease=false # Get short SHA for all builds short_sha=$(git rev-parse --short HEAD) # Determine build type based on trigger if [[ "${{ startsWith(github.ref, 'refs/tags/') }}" == "true" ]]; then # Tag push - release or prerelease should_build=true 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 "๐Ÿš€ Prerelease build detected: $tag_name" else build_type="release" echo "๐Ÿ“ฆ Release build detected: $tag_name" fi elif [[ "${{ github.ref }}" == "refs/heads/main" ]]; then # Main branch push - development build should_build=true build_type="development" version="dev-${short_sha}" echo "๐Ÿ› ๏ธ Development build detected" elif [[ "${{ github.event_name }}" == "schedule" ]] || \ [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then # Scheduled or manual build should_build=true build_type="development" version="dev-${short_sha}" echo "โšก Manual/scheduled build detected" fi echo "should_build=$should_build" >> $GITHUB_OUTPUT echo "build_type=$build_type" >> $GITHUB_OUTPUT echo "version=$version" >> $GITHUB_OUTPUT echo "short_sha=$short_sha" >> $GITHUB_OUTPUT echo "is_prerelease=$is_prerelease" >> $GITHUB_OUTPUT echo "๐Ÿ“Š Build Summary:" echo " - Should build: $should_build" echo " - Build type: $build_type" echo " - Version: $version" echo " - Short SHA: $short_sha" echo " - Is prerelease: $is_prerelease" # Build RustFS binaries prepare-platform-matrix: name: Prepare Platform Matrix needs: build-check runs-on: ubuntu-latest outputs: matrix: ${{ steps.select.outputs.matrix }} selected: ${{ steps.select.outputs.selected }} steps: - name: Select target platforms id: select shell: bash run: | set -euo pipefail selected="${{ github.event_name == 'workflow_dispatch' && github.event.inputs.platforms || 'all' }}" selected="$(echo "${selected}" | tr -d '[:space:]')" if [[ -z "${selected}" ]]; then selected="all" fi # Per-merge development builds only feed the dev download channel # and the Docker dev images, which consume the Linux binaries; at # the usual merge cadence most per-merge builds are cancelled by the # next merge anyway. macOS and Windows stay covered by tag builds, # the weekly scheduled build and manual dispatch. if [[ "${selected}" == "all" \ && "${{ github.event_name }}" == "push" \ && "${{ needs.build-check.outputs.build_type }}" == "development" ]]; then selected="linux-x86_64-musl,linux-aarch64-musl,linux-x86_64-gnu,linux-aarch64-gnu" echo "Development (main push) build: restricting to Linux targets" fi all='{"include":[ {"target_id":"linux-x86_64-musl","os":"sm-standard-2","target":"x86_64-unknown-linux-musl","cross":false,"platform":"linux","rustflags":""}, {"target_id":"linux-aarch64-musl","os":"sm-standard-2","target":"aarch64-unknown-linux-musl","cross":true,"platform":"linux","rustflags":""}, {"target_id":"linux-x86_64-gnu","os":"sm-standard-2","target":"x86_64-unknown-linux-gnu","cross":false,"platform":"linux","rustflags":""}, {"target_id":"linux-aarch64-gnu","os":"sm-standard-2","target":"aarch64-unknown-linux-gnu","cross":true,"platform":"linux","rustflags":""}, {"target_id":"macos-aarch64","os":"macos-latest","target":"aarch64-apple-darwin","cross":false,"platform":"macos","rustflags":""}, {"target_id":"macos-x86_64","os":"macos-26-intel","target":"x86_64-apple-darwin","cross":false,"platform":"macos","rustflags":""}, {"target_id":"windows-x86_64","os":"windows-latest","target":"x86_64-pc-windows-msvc","cross":false,"platform":"windows","rustflags":""} ]}' if [[ "${selected}" == "all" ]]; then matrix="$(jq -c . <<<"${all}")" else unknown="$(jq -rn --arg selected "${selected}" --argjson all "${all}" ' ($selected | split(",") | map(select(length > 0))) as $req | ($all.include | map(.target_id)) as $known | [$req[] | select(( $known | index(.) ) == null)] ')" if [[ "$(jq 'length' <<<"${unknown}")" -gt 0 ]]; then echo "Unknown platforms: $(jq -r 'join(\",\")' <<<"${unknown}")" >&2 echo "Allowed: $(jq -r '.include[].target_id' <<<"${all}" | paste -sd ',' -)" >&2 exit 1 fi matrix="$(jq -c --arg selected "${selected}" ' ($selected | split(",") | map(select(length > 0))) as $req | .include |= map(select(.target_id as $id | ($req | index($id)))) ' <<<"${all}")" fi echo "selected=${selected}" >> "$GITHUB_OUTPUT" echo "matrix=${matrix}" >> "$GITHUB_OUTPUT" echo "Selected platforms: ${selected}" build-rustfs: name: Build RustFS needs: [ build-check, prepare-platform-matrix ] if: needs.build-check.outputs.should_build == 'true' && needs.prepare-platform-matrix.result == 'success' runs-on: ${{ matrix.platform == 'linux' && fromJSON('["self-hosted","linux","sm-standard-2"]') || matrix.os }} timeout-minutes: 90 env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" # Release binaries ship without dial9 telemetry and therefore do not need # --cfg tokio_unstable. Telemetry builds opt in explicitly (see # `make build-profiling`); crates/obs/build.rs fails the compile if the # `dial9` feature is enabled without the flag. RUSTFLAGS: "${{ matrix.rustflags }}" strategy: fail-fast: false matrix: ${{ fromJson(needs.prepare-platform-matrix.outputs.matrix) }} steps: - name: Checkout repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: fetch-depth: 0 - name: Setup Rust environment uses: ./.github/actions/setup with: rust-version: stable target: ${{ matrix.target }} cache-shared-key: build-${{ matrix.target }} github-token: ${{ secrets.GITHUB_TOKEN }} cache-save-if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') }} install-cross-tools: ${{ matrix.cross }} - name: Download static console assets shell: bash env: GITHUB_TOKEN: ${{ github.token }} run: | mkdir -p ./rustfs/static verify_sha256() { local expected="$1" local file="$2" local actual="" local actual_line="" if command -v sha256sum >/dev/null 2>&1; then actual_line=$(sha256sum -- "$file") || return 1 elif command -v shasum >/dev/null 2>&1; then actual_line=$(shasum -a 256 -- "$file") || return 1 else echo "No SHA-256 verification tool found" >&2 return 1 fi actual="${actual_line%%[[:space:]]*}" if [ "$actual" = "$expected" ]; then echo "SHA256 verified OK: $actual" return 0 else echo "SHA256 mismatch: expected=$expected actual=$actual" >&2 return 1 fi } download_console_assets() { local curl_bin="curl" local python_bin="python3" local console_api="https://api.github.com/repos/rustfs/console/releases/latest" local console_json="console-release.json" local console_url local console_sha256 local curl_auth_args=() if [[ "${{ matrix.platform }}" == "windows" ]]; then curl_bin="curl.exe" else chmod +w ./rustfs/static/LICENSE || true fi if ! command -v "$python_bin" >/dev/null 2>&1; then python_bin="python" fi if ! command -v "$python_bin" >/dev/null 2>&1; then echo "No Python interpreter found for release metadata parsing" >&2 return 1 fi if [[ -n "${GITHUB_TOKEN:-}" ]]; then curl_auth_args=(-H "Authorization: Bearer ${GITHUB_TOKEN}" -H "X-GitHub-Api-Version: 2022-11-28") fi "$curl_bin" "${curl_auth_args[@]}" --fail -L "$console_api" \ -o "$console_json" --retry 3 --retry-delay 5 --max-time 300 || return 1 read -r console_url console_sha256 < <("$python_bin" - "$console_json" <<'PY' import json import re import sys with open(sys.argv[1], encoding="utf-8") as handle: release = json.load(handle) for asset in release.get("assets", []): name = asset.get("name", "") digest = asset.get("digest", "") url = asset.get("browser_download_url", "") if name.startswith("rustfs-console-") and name.endswith(".zip") and digest.startswith("sha256:") and url: sha256 = digest.split(":", 1)[1] if not re.fullmatch(r"[0-9a-fA-F]{64}", sha256): raise SystemExit(f"console zip asset has invalid sha256 digest: {sha256}") sys.stdout.buffer.write(f"{url} {sha256}\n".encode("utf-8")) break else: raise SystemExit("no console zip asset with sha256 digest found") PY ) || return 1 if [[ -z "$console_url" || -z "$console_sha256" ]]; then echo "Console release metadata is missing URL or SHA-256 digest" >&2 return 1 fi "$curl_bin" --fail -L "$console_url" -o console.zip --retry 3 --retry-delay 5 --max-time 300 || return 1 verify_sha256 "$console_sha256" console.zip || return 2 unzip -o console.zip -d ./rustfs/static || return 2 } if download_console_assets; then rm -f console.zip console-release.json else status=$? rm -f console.zip console-release.json if [[ "$status" -eq 2 ]]; then echo "Console asset integrity verification failed" >&2 exit 1 fi echo "Warning: Failed to download verified console assets, continuing without them" echo "// Static assets not available" > ./rustfs/static/empty.txt fi - name: Build RustFS shell: bash run: | # Force rebuild by touching build.rs touch rustfs/build.rs if [[ "${{ matrix.cross }}" == "true" ]]; then # All cross targets in the matrix are Linux; zigbuild handles them. cargo zigbuild --release --target ${{ matrix.target }} -p rustfs --bins else cargo build --release --target ${{ matrix.target }} -p rustfs --bins fi - name: Create release package id: package shell: bash run: | BUILD_TYPE="${{ needs.build-check.outputs.build_type }}" VERSION="${{ needs.build-check.outputs.version }}" SHORT_SHA="${{ needs.build-check.outputs.short_sha }}" # Extract platform and arch from target TARGET="${{ matrix.target }}" PLATFORM="${{ matrix.platform }}" # Map target to architecture and variant case "$TARGET" in *x86_64*musl*) ARCH="x86_64" VARIANT="musl" ;; *x86_64*gnu*) ARCH="x86_64" VARIANT="gnu" ;; *x86_64*) ARCH="x86_64" VARIANT="" ;; *aarch64*musl*|*arm64*musl*) ARCH="aarch64" VARIANT="musl" ;; *aarch64*gnu*|*arm64*gnu*) ARCH="aarch64" VARIANT="gnu" ;; *aarch64*|*arm64*) ARCH="aarch64" VARIANT="" ;; *armv7*) ARCH="armv7" VARIANT="" ;; *) ARCH="unknown" VARIANT="" ;; esac # Normalize version used for package filenames PACKAGE_VERSION="${VERSION}" if [[ "$PACKAGE_VERSION" == v* ]]; then PACKAGE_VERSION="${PACKAGE_VERSION#v}" fi # Generate package name based on build type if [[ -n "$VARIANT" ]]; then ARCH_WITH_VARIANT="${ARCH}-${VARIANT}" else ARCH_WITH_VARIANT="${ARCH}" fi PACKAGE_BASENAME="rustfs-${PLATFORM}-${ARCH_WITH_VARIANT}" if [[ "$BUILD_TYPE" == "development" ]]; then # Development build: rustfs-${platform}-${arch}-${variant}-dev-${short_sha}.zip PACKAGE_NAME="rustfs-${PLATFORM}-${ARCH_WITH_VARIANT}-dev-${SHORT_SHA}" else # Release/Prerelease build: rustfs-${platform}-${arch}-${variant}-v${version}.zip PACKAGE_NAME="${PACKAGE_BASENAME}-v${PACKAGE_VERSION}" fi # Create zip packages for all platforms # Ensure zip is available if ! command -v zip &> /dev/null; then if [[ "${{ matrix.os }}" == "ubuntu-latest" ]]; then sudo apt-get update && sudo apt-get install -y zip fi fi cd target/${{ matrix.target }}/release # Determine the binary name based on platform if [[ "${{ matrix.platform }}" == "windows" ]]; then BINARY_NAME="rustfs.exe" else BINARY_NAME="rustfs" fi # Verify the binary exists before packaging if [[ ! -f "$BINARY_NAME" ]]; then echo "โŒ Binary $BINARY_NAME not found in $(pwd)" if [[ "${{ matrix.platform }}" == "windows" ]]; then dir else ls -la fi exit 1 fi # Universal packaging function package_zip() { local src=$1 local dst=$2 if [[ "${{ matrix.platform }}" == "windows" ]]; then # Windows uses PowerShell Compress-Archive powershell -Command "Compress-Archive -Path '$src' -DestinationPath '$dst' -Force" elif command -v zip &> /dev/null; then # Unix systems use zip command zip "$dst" "$src" else echo "โŒ No zip utility available" exit 1 fi } # Create the zip package echo "Start packaging: $BINARY_NAME -> ../../../${PACKAGE_NAME}.zip" package_zip "$BINARY_NAME" "../../../${PACKAGE_NAME}.zip" cd ../../.. # Verify the package was created if [[ -f "${PACKAGE_NAME}.zip" ]]; then echo "โœ… Package created successfully: ${PACKAGE_NAME}.zip" if [[ "${{ matrix.platform }}" == "windows" ]]; then dir else ls -lh ${PACKAGE_NAME}.zip fi else echo "โŒ Failed to create package: ${PACKAGE_NAME}.zip" exit 1 fi # Create latest version files right after the main package LATEST_FILES="" if [[ "$BUILD_TYPE" == "release" ]] || [[ "$BUILD_TYPE" == "prerelease" ]]; then # Create latest version filename # Convert from rustfs-linux-x86_64-musl-v1.0.0 to rustfs-linux-x86_64-musl-latest LATEST_FILE="${PACKAGE_BASENAME}-latest.zip" echo "๐Ÿ”„ Creating latest version: ${PACKAGE_NAME}.zip -> $LATEST_FILE" cp "${PACKAGE_NAME}.zip" "$LATEST_FILE" if [[ -f "$LATEST_FILE" ]]; then echo "โœ… Latest version created: $LATEST_FILE" LATEST_FILES="$LATEST_FILE" fi elif [[ "$BUILD_TYPE" == "development" ]]; then # Development builds (only main branch triggers development builds) # Create main-latest version filename # Convert from rustfs-linux-x86_64-dev-abc123 to rustfs-linux-x86_64-main-latest MAIN_LATEST_FILE="${PACKAGE_BASENAME}-main-latest.zip" echo "๐Ÿ”„ Creating main-latest version: ${PACKAGE_NAME}.zip -> $MAIN_LATEST_FILE" cp "${PACKAGE_NAME}.zip" "$MAIN_LATEST_FILE" if [[ -f "$MAIN_LATEST_FILE" ]]; then echo "โœ… Main-latest version created: $MAIN_LATEST_FILE" LATEST_FILES="$MAIN_LATEST_FILE" # Also create a generic main-latest for Docker builds (Linux only) if [[ "${{ matrix.platform }}" == "linux" ]]; then DOCKER_MAIN_LATEST_FILE="rustfs-linux-${ARCH_WITH_VARIANT}-main-latest.zip" echo "๐Ÿ”„ Creating Docker main-latest version: ${PACKAGE_NAME}.zip -> $DOCKER_MAIN_LATEST_FILE" cp "${PACKAGE_NAME}.zip" "$DOCKER_MAIN_LATEST_FILE" if [[ -f "$DOCKER_MAIN_LATEST_FILE" ]]; then echo "โœ… Docker main-latest version created: $DOCKER_MAIN_LATEST_FILE" LATEST_FILES="$LATEST_FILES $DOCKER_MAIN_LATEST_FILE" fi fi fi fi echo "package_name=${PACKAGE_NAME}" >> $GITHUB_OUTPUT echo "package_file=${PACKAGE_NAME}.zip" >> $GITHUB_OUTPUT echo "latest_files=${LATEST_FILES}" >> $GITHUB_OUTPUT echo "build_type=${BUILD_TYPE}" >> $GITHUB_OUTPUT echo "version=${VERSION}" >> $GITHUB_OUTPUT echo "๐Ÿ“ฆ Package created: ${PACKAGE_NAME}.zip" if [[ -n "$LATEST_FILES" ]]; then echo "๐Ÿ“ฆ Latest files created: $LATEST_FILES" fi echo "๐Ÿ”ง Build type: ${BUILD_TYPE}" echo "๐Ÿ“Š Version: ${VERSION}" - name: Upload to GitHub artifacts uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 with: name: ${{ steps.package.outputs.package_name }} path: "rustfs-*.zip" retention-days: ${{ startsWith(github.ref, 'refs/tags/') && 30 || 7 }} - name: Upload to Cloudflare R2 if: env.R2_ACCESS_KEY_ID != '' && (needs.build-check.outputs.build_type == 'release' || needs.build-check.outputs.build_type == 'prerelease' || needs.build-check.outputs.build_type == 'development') env: R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }} R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }} R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }} R2_BUCKET: ${{ secrets.R2_BUCKET }} AWS_EC2_METADATA_DISABLED: true shell: bash run: | BUILD_TYPE="${{ needs.build-check.outputs.build_type }}" if [[ -z "$R2_ACCESS_KEY_ID" || -z "$R2_SECRET_ACCESS_KEY" || -z "$R2_ENDPOINT" || -z "$R2_BUCKET" ]]; then echo "โš ๏ธ R2 credentials or endpoint missing, skipping artifact upload" exit 0 fi if ! command -v aws >/dev/null 2>&1; then echo "โŒ aws CLI not found on runner; cannot upload to R2" exit 1 fi export AWS_ACCESS_KEY_ID="$R2_ACCESS_KEY_ID" export AWS_SECRET_ACCESS_KEY="$R2_SECRET_ACCESS_KEY" export AWS_DEFAULT_REGION="auto" # Determine upload path based on build type if [[ "$BUILD_TYPE" == "development" ]]; then R2_PATH="s3://${R2_BUCKET}/artifacts/rustfs/dev/" echo "๐Ÿ“ค Uploading development build to R2 dev directory" else R2_PATH="s3://${R2_BUCKET}/artifacts/rustfs/release/" echo "๐Ÿ“ค Uploading release build to R2 release directory" fi # Upload all rustfs zip files to R2 using S3-compatible API echo "๐Ÿ“ค Uploading all rustfs-*.zip files to $R2_PATH..." for zip_file in rustfs-*.zip; do if [[ -f "$zip_file" ]]; then echo "Uploading: $zip_file to $R2_PATH..." aws s3 cp "$zip_file" "$R2_PATH" --endpoint-url "$R2_ENDPOINT" --only-show-errors echo "โœ… Uploaded: $zip_file" fi done echo "โœ… Upload completed successfully" # Build summary build-summary: name: Build Summary needs: [ build-check, build-rustfs ] if: always() && needs.build-check.outputs.should_build == 'true' runs-on: ubuntu-latest steps: - name: Build completion summary shell: bash run: | BUILD_TYPE="${{ needs.build-check.outputs.build_type }}" VERSION="${{ needs.build-check.outputs.version }}" echo "๐ŸŽ‰ Build completed successfully!" echo "๐Ÿ“ฆ Build type: $BUILD_TYPE" 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" echo "โš ๏ธ This is a development build - not suitable for production use" ;; "release") echo "๐Ÿš€ Release build artifacts have been uploaded to OSS release directory" echo "โœ… This build is ready for production use" echo "๐Ÿท๏ธ GitHub Release will be created in this workflow" ;; "prerelease") echo "๐Ÿงช Prerelease build artifacts have been uploaded to OSS release directory" echo "โš ๏ธ This is a prerelease build - use with caution" echo "๐Ÿท๏ธ GitHub Release will be created in this 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 # Create GitHub Release (only for tag pushes) create-release: name: Create GitHub Release needs: [ build-check, build-rustfs ] if: startsWith(github.ref, 'refs/tags/') && needs.build-check.outputs.build_type != 'development' runs-on: ubuntu-latest permissions: contents: write outputs: release_id: ${{ steps.create.outputs.release_id }} release_url: ${{ steps.create.outputs.release_url }} steps: - name: Checkout repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: fetch-depth: 0 - name: Create GitHub Release id: create env: GH_TOKEN: ${{ github.token }} shell: bash run: | TAG="${{ needs.build-check.outputs.version }}" VERSION="${{ needs.build-check.outputs.version }}" IS_PRERELEASE="${{ needs.build-check.outputs.is_prerelease }}" BUILD_TYPE="${{ needs.build-check.outputs.build_type }}" # Determine release type for title if [[ "$BUILD_TYPE" == "prerelease" ]]; then if [[ "$TAG" == *"alpha"* ]]; then RELEASE_TYPE="alpha" elif [[ "$TAG" == *"beta"* ]]; then RELEASE_TYPE="beta" elif [[ "$TAG" == *"rc"* ]]; then RELEASE_TYPE="rc" else RELEASE_TYPE="prerelease" fi else RELEASE_TYPE="release" fi # Check if release already exists if gh release view "$TAG" >/dev/null 2>&1; then echo "Release $TAG already exists" RELEASE_ID=$(gh release view "$TAG" --json databaseId --jq '.databaseId') RELEASE_URL=$(gh release view "$TAG" --json url --jq '.url') else # Get release notes from tag message RELEASE_NOTES=$(git tag -l --format='%(contents)' "${TAG}") if [[ -z "$RELEASE_NOTES" || "$RELEASE_NOTES" =~ ^[[:space:]]*$ ]]; then if [[ "$IS_PRERELEASE" == "true" ]]; then RELEASE_NOTES="Pre-release ${VERSION} (${RELEASE_TYPE})" else RELEASE_NOTES="Release ${VERSION}" fi fi # Create release title if [[ "$IS_PRERELEASE" == "true" ]]; then TITLE="RustFS $VERSION (${RELEASE_TYPE})" else TITLE="RustFS $VERSION" fi # Create the release PRERELEASE_FLAG="" if [[ "$IS_PRERELEASE" == "true" ]]; then PRERELEASE_FLAG="--prerelease" fi gh release create "$TAG" \ --title "$TITLE" \ --notes "$RELEASE_NOTES" \ $PRERELEASE_FLAG \ --draft RELEASE_ID=$(gh release view "$TAG" --json databaseId --jq '.databaseId') RELEASE_URL=$(gh release view "$TAG" --json url --jq '.url') fi echo "release_id=$RELEASE_ID" >> $GITHUB_OUTPUT echo "release_url=$RELEASE_URL" >> $GITHUB_OUTPUT echo "Created release: $RELEASE_URL" # Prepare and upload release assets upload-release-assets: name: Upload Release Assets needs: [ build-check, build-rustfs, create-release ] if: startsWith(github.ref, 'refs/tags/') && needs.build-check.outputs.build_type != 'development' runs-on: ubuntu-latest permissions: contents: write actions: read steps: - name: Checkout repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - name: Download all build artifacts uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 with: path: ./artifacts pattern: rustfs-* merge-multiple: true - name: Prepare release assets id: prepare shell: bash run: | VERSION="${{ needs.build-check.outputs.version }}" TAG="${{ needs.build-check.outputs.version }}" mkdir -p ./release-assets # Copy and verify artifacts (including latest files created during build) ASSETS_COUNT=0 for file in ./artifacts/*.zip; do if [[ -f "$file" ]]; then cp "$file" ./release-assets/ ASSETS_COUNT=$((ASSETS_COUNT + 1)) fi done if [[ $ASSETS_COUNT -eq 0 ]]; then echo "โŒ No artifacts found!" exit 1 fi cd ./release-assets # Generate checksums for all files (including latest versions) if ls *.zip >/dev/null 2>&1; then sha256sum *.zip > SHA256SUMS sha512sum *.zip > SHA512SUMS fi cd .. python3 scripts/security/generate_release_supply_chain_assets.py \ --asset-dir ./release-assets \ --version "$VERSION" \ --tag "$TAG" \ --build-type "${{ needs.build-check.outputs.build_type }}" \ --repository "${{ github.repository }}" \ --ref "${{ github.ref }}" \ --sha "${{ github.sha }}" \ --run-id "${{ github.run_id }}" \ --run-attempt "${{ github.run_attempt }}" cd ./release-assets echo "๐Ÿ“ฆ Prepared assets:" ls -la echo "๐Ÿ”ข Total asset count: $ASSETS_COUNT" - name: Upload to GitHub Release env: GH_TOKEN: ${{ github.token }} shell: bash run: | TAG="${{ needs.build-check.outputs.version }}" cd ./release-assets # Upload all files for file in *; do if [[ -f "$file" ]]; then echo "๐Ÿ“ค Uploading $file..." gh release upload "$TAG" "$file" --clobber fi done echo "โœ… All assets uploaded successfully" # Update latest.json for stable releases only: prerelease tags (alpha/beta/ # rc) must never overwrite the stable version pointer. update-latest-version: name: Update Latest Version needs: [ build-check, upload-release-assets ] if: startsWith(github.ref, 'refs/tags/') && needs.build-check.outputs.build_type == 'release' runs-on: ubuntu-latest steps: - name: Update latest.json env: OSS_ACCESS_KEY_ID: ${{ secrets.ALICLOUDOSS_KEY_ID }} OSS_ACCESS_KEY_SECRET: ${{ secrets.ALICLOUDOSS_KEY_SECRET }} OSS_REGION: cn-beijing OSS_ENDPOINT: https://oss-cn-beijing.aliyuncs.com shell: bash run: | if [[ -z "$OSS_ACCESS_KEY_ID" ]]; then echo "โš ๏ธ OSS credentials not available, skipping latest.json update" exit 0 fi VERSION="${{ needs.build-check.outputs.version }}" TAG="${{ needs.build-check.outputs.version }}" # Install ossutil OSSUTIL_VERSION="2.1.1" OSSUTIL_ZIP="ossutil-${OSSUTIL_VERSION}-linux-amd64.zip" OSSUTIL_DIR="ossutil-${OSSUTIL_VERSION}-linux-amd64" OSSUTIL_SHA256="60f3a808cbbdfd4b5269b8f967c6a66f564c9dacff52d93a5d21a588976ab5a2" curl --fail -L -o "$OSSUTIL_ZIP" "https://gosspublic.alicdn.com/ossutil/v2/${OSSUTIL_VERSION}/${OSSUTIL_ZIP}" printf '%s %s\n' "$OSSUTIL_SHA256" "$OSSUTIL_ZIP" | sha256sum -c - unzip "$OSSUTIL_ZIP" OSSUTIL_BIN="${RUNNER_TEMP}/ossutil" mv "${OSSUTIL_DIR}/ossutil" "$OSSUTIL_BIN" rm -rf "$OSSUTIL_DIR" "$OSSUTIL_ZIP" chmod +x "$OSSUTIL_BIN" # Create latest.json cat > latest.json << EOF { "version": "${VERSION}", "tag": "${TAG}", "release_date": "$(date -u +%Y-%m-%dT%H:%M:%SZ)", "release_type": "stable", "download_url": "https://github.com/${{ github.repository }}/releases/tag/${TAG}" } EOF # Upload to OSS "$OSSUTIL_BIN" cp latest.json oss://rustfs-version/latest.json --force echo "โœ… Updated latest.json for stable release $VERSION" # Publish release (remove draft status) publish-release: name: Publish Release needs: [ build-check, create-release, upload-release-assets ] if: startsWith(github.ref, 'refs/tags/') && needs.build-check.outputs.build_type != 'development' runs-on: ubuntu-latest permissions: contents: write steps: - name: Checkout repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - name: Update release notes and publish env: GH_TOKEN: ${{ github.token }} shell: bash run: | TAG="${{ needs.build-check.outputs.version }}" VERSION="${{ needs.build-check.outputs.version }}" IS_PRERELEASE="${{ needs.build-check.outputs.is_prerelease }}" BUILD_TYPE="${{ needs.build-check.outputs.build_type }}" # Determine release type if [[ "$BUILD_TYPE" == "prerelease" ]]; then if [[ "$TAG" == *"alpha"* ]]; then RELEASE_TYPE="alpha" elif [[ "$TAG" == *"beta"* ]]; then RELEASE_TYPE="beta" elif [[ "$TAG" == *"rc"* ]]; then RELEASE_TYPE="rc" else RELEASE_TYPE="prerelease" fi else RELEASE_TYPE="release" fi # Get original release notes from tag ORIGINAL_NOTES=$(git tag -l --format='%(contents)' "${TAG}") if [[ -z "$ORIGINAL_NOTES" || "$ORIGINAL_NOTES" =~ ^[[:space:]]*$ ]]; then if [[ "$IS_PRERELEASE" == "true" ]]; then ORIGINAL_NOTES="Pre-release ${VERSION} (${RELEASE_TYPE})" else ORIGINAL_NOTES="Release ${VERSION}" fi fi # Publish the release (remove draft status) gh release edit "$TAG" --draft=false echo "๐ŸŽ‰ Released $TAG successfully!" echo "๐Ÿ“„ Release URL: ${{ needs.create-release.outputs.release_url }}"