mirror of
https://github.com/rustfs/rustfs.git
synced 2026-11-01 08:40:23 +00:00
c7848df9a0
* fix(build): pin gnu glibc baseline to 2.31 and report compile target in version metadata gnu release binaries were built natively on Ubuntu 24.04, embedding GLIBC_2.38/2.39 symbol references while the RPM/DEB metadata promised glibc >= 2.31, so Rocky/RHEL 9 (glibc 2.34) could not run them (#8062). The two linux-gnu legs now build through cargo-zigbuild pinned to the 2.31 baseline (same-arch; zig acts as the linker), with post-build guards asserting both the embedded version metadata and the highest referenced GLIBC symbol version, keeping the package declarations honest. Version metadata itself now comes from cargo's TARGET cfg instead of shadow-rs' host stamping, so builds whose host differs from their target (cross compilation, musl targets on a glibc-hosted toolchain) report the platform they were compiled for (#8048 follow-up). The cross toolchain (zig 0.14.x, cargo-zigbuild 0.23.4) is pinned in the setup action now that it is load-bearing for releases, the build job gets an explicit matrix-independent name so the connect-* acceptance workflows keep matching it when matrix fields change, and the preview-release workflow contract is updated for the TARGET_ARG build commands. Fixes #8062 Follow-up on #8048 * fix(build): enforce GNU ABI floor and canonical target metadata Pin Zig 0.17.0 and cargo-zigbuild, isolate cross-toolchain caches, and build GNU releases against the declared glibc 2.31 floor. Validate both release ELF binaries using their loader version requirements. Generate target metadata once through shadow-rs and check the actual release version output without adding a parallel metadata facade. Refs: rustfs/backlog#2824 Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> * perf(build): reuse GNU release profile across binaries Switching the CLI back to the default strip profile restarted 419 compilation units after the server build. Keep the profiling build profile for both binaries, then strip only the CLI artifact. Retain serial links, server profiling symbols, and post-strip ABI gates. Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> * perf(build): limit release artifacts to the rustfs server Build only rustfs in the release workflow and remove the CLI build, strip, and packaging paths. Preserve GNU profiling symbols and catalog sidecars, and align strict acceptance archive readers with the new set. Exercise all six build targets, Linux package checks, and all four strict acceptance extractors using their actual workflow scripts. Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> --------- Co-authored-by: majinghe <42570491+majinghe@users.noreply.github.com> Co-authored-by: heihutu <heihutu@gmail.com> Co-authored-by: zhi22915 <qiuzgang@gmail.com>
1211 lines
48 KiB
YAML
1211 lines
48 KiB
YAML
# 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:
|
||
# - publish: Publish binaries/releases for manual runs (default: false)
|
||
# - build_docker: Advisory only; manual runs do not trigger Docker publishing
|
||
# - 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"
|
||
- "flake.lock"
|
||
schedule:
|
||
- cron: "13 1 * * 0" # Weekly on Sunday 01:13 UTC
|
||
workflow_dispatch:
|
||
inputs:
|
||
publish:
|
||
description: "Publish to download channels and GitHub Releases (false keeps only workflow artifacts)"
|
||
required: false
|
||
default: false
|
||
type: boolean
|
||
build_docker:
|
||
# Advisory only. docker.yml triggers on workflow_run and its job-level
|
||
# condition requires the triggering event to be a tag push, so a manual
|
||
# dispatch of this workflow never produces images regardless of this
|
||
# value. Publishing remains controlled by the explicit publish input.
|
||
description: "Build and push Docker images after binary build (ignored: dispatch runs never reach docker.yml)"
|
||
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: sm-standard-2
|
||
timeout-minutes: 30
|
||
outputs:
|
||
should_build: ${{ steps.check.outputs.should_build }}
|
||
should_publish: ${{ github.event_name != 'workflow_dispatch' || inputs.publish == true }}
|
||
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@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||
with:
|
||
persist-credentials: false
|
||
|
||
- 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 - preview, release, or prerelease
|
||
should_build=true
|
||
tag_name="${GITHUB_REF#refs/tags/}"
|
||
version="${tag_name}"
|
||
|
||
# Preview tags publish a GitHub prerelease for validation, but
|
||
# must not update any latest channel.
|
||
if [[ "$tag_name" =~ -preview\.[0-9]+$ ]]; then
|
||
build_type="preview"
|
||
is_prerelease=true
|
||
echo "🔍 Preview build detected: $tag_name"
|
||
elif [[ "$tag_name" == *"-preview"* ]]; then
|
||
echo "❌ Invalid preview tag: $tag_name (expected suffix: -preview.<number>)" >&2
|
||
exit 1
|
||
elif [[ "$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"
|
||
echo "build_type=$build_type"
|
||
echo "version=$version"
|
||
echo "short_sha=$short_sha"
|
||
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: sm-standard-2
|
||
timeout-minutes: 30
|
||
outputs:
|
||
matrix: ${{ steps.select.outputs.matrix }}
|
||
selected: ${{ steps.select.outputs.selected }}
|
||
steps:
|
||
- name: Select target platforms
|
||
id: select
|
||
shell: bash
|
||
env:
|
||
# via env, not interpolation: a dispatch input is free-form text and
|
||
# would otherwise be pasted into the script for bash to evaluate.
|
||
RAW_PLATFORMS: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.platforms || 'all' }}
|
||
run: |
|
||
set -euo pipefail
|
||
|
||
selected="$RAW_PLATFORMS"
|
||
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-4","target":"x86_64-unknown-linux-musl","cross":false,"platform":"linux","rustflags":"","features":"","glibc":""},
|
||
{"target_id":"linux-aarch64-musl","os":"sm-standard-4-arm","target":"aarch64-unknown-linux-musl","cross":false,"platform":"linux","rustflags":"","features":"","glibc":""},
|
||
{"target_id":"linux-x86_64-gnu","os":"sm-standard-4","target":"x86_64-unknown-linux-gnu","cross":true,"platform":"linux","rustflags":"","features":"pyroscope","glibc":"2.31"},
|
||
{"target_id":"linux-aarch64-gnu","os":"sm-standard-4-arm","target":"aarch64-unknown-linux-gnu","cross":true,"platform":"linux","rustflags":"","features":"pyroscope","glibc":"2.31"},
|
||
{"target_id":"macos-aarch64","os":"macos-latest","target":"aarch64-apple-darwin","cross":false,"platform":"macos","rustflags":"","features":"pyroscope","glibc":""},
|
||
{"target_id":"windows-x86_64","os":"windows-latest","target":"x86_64-pc-windows-msvc","cross":false,"platform":"windows","rustflags":"","features":"","glibc":""}
|
||
]}'
|
||
|
||
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:
|
||
# Acceptance workflows select artifacts by this stable platform name.
|
||
name: Build RustFS (${{ matrix.target_id }})
|
||
needs: [ build-check, prepare-platform-matrix ]
|
||
if: needs.build-check.outputs.should_build == 'true' && needs.prepare-platform-matrix.result == 'success'
|
||
runs-on: ${{ matrix.os }}
|
||
timeout-minutes: 180
|
||
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@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||
with:
|
||
persist-credentials: false
|
||
fetch-depth: 0
|
||
|
||
- name: Setup Rust environment
|
||
uses: ./.github/actions/setup
|
||
with:
|
||
rust-version: stable
|
||
target: ${{ matrix.target }}
|
||
cache-shared-key: build-${{ matrix.target }}${{ matrix.glibc && format('-glibc-{0}', matrix.glibc) || '' }}
|
||
# main only. A cache saved on refs/tags/X is scoped to that tag: no
|
||
# other tag, no main run and no PR can restore it, so every release
|
||
# cycle wrote up to 12 entries of 1-2GB (preview tag plus final tag,
|
||
# six legs each) that nobody could read, evicting the hot lanes from
|
||
# the repo-wide 10GB quota. Tag builds still restore the main-scoped
|
||
# cache, since default-branch caches are readable from every ref.
|
||
# The one real cost: re-running a failed leg of the same tag no longer
|
||
# finds that tag's own warm cache and falls back to main's.
|
||
cache-save-if: ${{ github.ref == 'refs/heads/main' }}
|
||
install-cross-tools: ${{ matrix.cross }}
|
||
install-test-tools: 'false'
|
||
|
||
- 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=()
|
||
console_tag=""
|
||
|
||
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_tag 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)
|
||
|
||
tag = release.get("tag_name", "") or "unknown"
|
||
|
||
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"{tag} {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
|
||
|
||
echo "Console release: ${console_tag}"
|
||
echo "Downloading console asset: ${console_url}"
|
||
"$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
|
||
fi
|
||
echo "Failed to download verified console assets" >&2
|
||
exit 1
|
||
fi
|
||
|
||
if [[ ! -s ./rustfs/static/index.html ]]; then
|
||
echo "Console asset archive is missing static/index.html" >&2
|
||
exit 1
|
||
fi
|
||
|
||
asset_count=$(find ./rustfs/static -type f | wc -l | tr -d '[:space:]')
|
||
echo "Console assets ready: version=${console_tag:-unknown}, ${asset_count} files extracted to ./rustfs/static"
|
||
|
||
- name: Build RustFS
|
||
shell: bash
|
||
env:
|
||
BUILD_TARGET: ${{ matrix.target }}
|
||
BUILD_OS: ${{ matrix.platform }}-${{ startsWith(matrix.target, 'aarch64-') && 'aarch64' || 'x86_64' }}
|
||
run: |
|
||
# Force rebuild by touching build.rs
|
||
touch rustfs/build.rs
|
||
|
||
FEATURE_ARGS=()
|
||
if [[ -n "${{ matrix.features }}" ]]; then
|
||
FEATURE_ARGS=(--features "${{ matrix.features }}")
|
||
fi
|
||
|
||
# gnu targets pin an explicit glibc baseline (`<target>.<glibc>` for
|
||
# cargo-zigbuild) so release binaries link no symbols newer than it
|
||
# and run on older distros such as RHEL/Rocky 9 (glibc 2.34); #8062.
|
||
TARGET_ARG="$BUILD_TARGET"
|
||
if [[ -n "${{ matrix.glibc }}" ]]; then
|
||
TARGET_ARG="${TARGET_ARG}.${{ matrix.glibc }}"
|
||
fi
|
||
|
||
PROFILE_ARGS=()
|
||
if [[ "$BUILD_TARGET" == *-linux-gnu && "${{ matrix.features }}" == "pyroscope" ]]; then
|
||
# The in-process profiler resolves names from the final executable.
|
||
# Retain function symbols without enabling DWARF source information.
|
||
PROFILE_ARGS=(--config 'profile.release.strip="none"')
|
||
fi
|
||
if [[ "${{ matrix.cross }}" == "true" ]]; then
|
||
cargo zigbuild --release --locked --target "$TARGET_ARG" "${FEATURE_ARGS[@]}" "${PROFILE_ARGS[@]}" -p rustfs --bin rustfs
|
||
else
|
||
cargo build --release --locked --target "$TARGET_ARG" "${FEATURE_ARGS[@]}" "${PROFILE_ARGS[@]}" -p rustfs --bin rustfs
|
||
fi
|
||
|
||
# All release targets use same-architecture runners, including GNU
|
||
# builds that use Zig only to select the glibc ABI floor.
|
||
bin="target/${{ matrix.target }}/release/rustfs"
|
||
if [[ -f "${bin}.exe" ]]; then
|
||
bin="${bin}.exe"
|
||
fi
|
||
version_output=$("$bin" --version)
|
||
if ! grep -Fxq "build target : ${BUILD_TARGET}" <<< "$version_output" || \
|
||
! grep -Fxq "build os : ${BUILD_OS}" <<< "$version_output" || \
|
||
! grep -Fxq "rust channel : stable-${BUILD_TARGET}" <<< "$version_output"; then
|
||
echo "release binary reports an unexpected compile target" >&2
|
||
exit 1
|
||
fi
|
||
if [[ -n "${{ matrix.glibc }}" ]]; then
|
||
python3 scripts/release/check_glibc.py "${{ matrix.glibc }}" \
|
||
"target/${{ matrix.target }}/release/rustfs"
|
||
fi
|
||
|
||
- name: Verify macOS binary has no dynamic liblzma dependency
|
||
if: matrix.platform == 'macos'
|
||
shell: bash
|
||
run: |
|
||
binary="target/${{ matrix.target }}/release/rustfs"
|
||
if otool -L "$binary" | grep -E 'liblzma.*\.dylib'; then
|
||
echo "macOS release binary must not depend on a dynamic liblzma library" >&2
|
||
exit 1
|
||
fi
|
||
"$binary" --version
|
||
|
||
- name: Generate the CPU symbol catalog
|
||
if: endsWith(matrix.target, '-linux-gnu') && matrix.features == 'pyroscope'
|
||
shell: bash
|
||
run: |
|
||
cargo metadata --locked --format-version 1 --filter-platform '${{ matrix.target }}' \
|
||
--features rustfs/pyroscope > "$RUNNER_TEMP/cpu-catalog-metadata.json"
|
||
features=$(jq -ce '
|
||
(.packages[] | select(.name == "rustfs") | .id) as $id |
|
||
.resolve.nodes[] | select(.id == $id) | .features | map(select(. != "default")) | sort
|
||
' "$RUNNER_TEMP/cpu-catalog-metadata.json")
|
||
release_dir="target/${{ matrix.target }}/release"
|
||
# These task-owned outputs may remain in a restored target cache.
|
||
rm -f "$release_dir/rustfs.cpu-symbol-catalog.json" "$release_dir/rustfs.cpu-symbol-catalog.sha256"
|
||
cargo run --locked -p rustfs-connect-symbol-catalog -- \
|
||
"$release_dir/rustfs" "$(git rev-parse HEAD)" '${{ matrix.target }}' "$features" \
|
||
"$release_dir/rustfs.cpu-symbol-catalog.json" > "$release_dir/rustfs.cpu-symbol-catalog.sha256"
|
||
|
||
- 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" || "${{ matrix.platform }}" == "linux" ]]; 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
|
||
|
||
BINARY_FILES=("$BINARY_NAME")
|
||
|
||
if [[ "$TARGET" == *-linux-gnu && "${{ matrix.features }}" == "pyroscope" ]]; then
|
||
BINARY_FILES+=(rustfs.cpu-symbol-catalog.json rustfs.cpu-symbol-catalog.sha256)
|
||
fi
|
||
|
||
# Verify the binaries exist before packaging
|
||
for binary in "${BINARY_FILES[@]}"; do
|
||
if [[ ! -f "$binary" ]]; then
|
||
echo "❌ Binary $binary not found in $(pwd)"
|
||
if [[ "${{ matrix.platform }}" == "windows" ]]; then
|
||
dir
|
||
else
|
||
ls -la
|
||
fi
|
||
exit 1
|
||
fi
|
||
done
|
||
|
||
# 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_FILES[*]} -> ../../../${PACKAGE_NAME}.zip"
|
||
if [[ "${{ matrix.platform }}" == "linux" ]]; then
|
||
zip "../../../${PACKAGE_NAME}.zip" "${BINARY_FILES[@]}"
|
||
else
|
||
package_zip "$BINARY_NAME" "../../../${PACKAGE_NAME}.zip"
|
||
fi
|
||
|
||
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}"
|
||
echo "package_file=${PACKAGE_NAME}.zip"
|
||
echo "latest_files=${LATEST_FILES}"
|
||
echo "build_type=${BUILD_TYPE}"
|
||
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: Verify packaged Linux binary
|
||
if: matrix.platform == 'linux'
|
||
shell: bash
|
||
run: |
|
||
set -euo pipefail
|
||
|
||
package_file="${{ steps.package.outputs.package_file }}"
|
||
archive_members="$(unzip -Z1 "$package_file" | sort)"
|
||
expected_members='rustfs'
|
||
target="${{ matrix.target }}"
|
||
features="${{ matrix.features }}"
|
||
if [[ "$target" == *-linux-gnu && "$features" == "pyroscope" ]]; then
|
||
expected_members+=$'\nrustfs.cpu-symbol-catalog.json\nrustfs.cpu-symbol-catalog.sha256'
|
||
fi
|
||
if [[ "$archive_members" != "$expected_members" ]]; then
|
||
echo "Linux package does not contain the expected files" >&2
|
||
printf '%s\n' "$archive_members" >&2
|
||
exit 1
|
||
fi
|
||
|
||
# musl targets build natively; the gnu legs are same-arch
|
||
# cargo-zigbuild builds pinned to the release glibc baseline. Either
|
||
# way the binaries run on their runner, so executing them here
|
||
# proves each artifact matches its runner architecture.
|
||
binary_dir="$(mktemp -d)"
|
||
trap 'rm -rf "$binary_dir"' EXIT
|
||
unzip -q "$package_file" rustfs -d "$binary_dir"
|
||
"$binary_dir/rustfs" --version
|
||
|
||
- name: Verify packaged console
|
||
if: matrix.target == 'x86_64-unknown-linux-gnu' || matrix.target == 'aarch64-unknown-linux-gnu'
|
||
shell: bash
|
||
run: |
|
||
set -euo pipefail
|
||
|
||
find_free_port() {
|
||
python3 - <<'PY'
|
||
import socket
|
||
|
||
with socket.socket() as sock:
|
||
sock.bind(("127.0.0.1", 0))
|
||
print(sock.getsockname()[1])
|
||
PY
|
||
}
|
||
|
||
package_dir="$(mktemp -d)"
|
||
data_dir="$(mktemp -d)"
|
||
log_file="${RUNNER_TEMP}/rustfs-console-smoke.log"
|
||
server_pid=""
|
||
trap '
|
||
if [[ -n "${server_pid:-}" ]]; then
|
||
kill "$server_pid" 2>/dev/null || true
|
||
wait "$server_pid" 2>/dev/null || true
|
||
fi
|
||
rm -rf "$package_dir" "$data_dir"
|
||
' EXIT
|
||
|
||
unzip -q "${{ steps.package.outputs.package_file }}" -d "$package_dir"
|
||
api_port="$(find_free_port)"
|
||
console_port="$(find_free_port)"
|
||
console_url="http://127.0.0.1:${console_port}/rustfs/console/"
|
||
|
||
"$package_dir/rustfs" server \
|
||
--address "127.0.0.1:${api_port}" \
|
||
--console-enable \
|
||
--console-address "127.0.0.1:${console_port}" \
|
||
--access-key console-smoke \
|
||
--secret-key console-smoke-secret \
|
||
"$data_dir" >"$log_file" 2>&1 &
|
||
server_pid=$!
|
||
|
||
for _ in {1..40}; do
|
||
response="$(curl --silent --output /dev/null --write-out '%{http_code} %{content_type}' "$console_url" || true)"
|
||
if [[ "$response" == 200\ text/html* ]]; then
|
||
exit 0
|
||
fi
|
||
sleep 0.25
|
||
done
|
||
|
||
echo "Console endpoint did not return 200 text/html: $response" >&2
|
||
cat "$log_file"
|
||
exit 1
|
||
|
||
- name: Upload to GitHub artifacts
|
||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||
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: needs.build-check.outputs.should_publish == 'true' && 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
|
||
|
||
# The self-hosted Linux runners do not ship the aws CLI (GitHub-hosted
|
||
# images did). Install the official CLI bundle on demand so R2 uploads
|
||
# survive a fresh runner without relying on a distro package.
|
||
if ! command -v aws >/dev/null 2>&1; then
|
||
echo "aws CLI not found on runner; installing..."
|
||
if command -v apt-get >/dev/null 2>&1; then
|
||
aws_arch="$(uname -m)"
|
||
case "$aws_arch" in
|
||
x86_64) aws_arch="x86_64" ;;
|
||
aarch64|arm64) aws_arch="aarch64" ;;
|
||
*)
|
||
echo "❌ Unsupported Linux architecture for AWS CLI: $aws_arch"
|
||
exit 1
|
||
;;
|
||
esac
|
||
|
||
aws_install_root="${RUNNER_TEMP:-$PWD}/aws-cli"
|
||
rm -rf "$aws_install_root"
|
||
mkdir -p "$aws_install_root"
|
||
curl --fail --location --retry 3 \
|
||
"https://awscli.amazonaws.com/awscli-exe-linux-${aws_arch}.zip" \
|
||
--output "$aws_install_root/awscliv2.zip"
|
||
unzip -q "$aws_install_root/awscliv2.zip" -d "$aws_install_root"
|
||
"$aws_install_root/aws/install" \
|
||
--install-dir "$aws_install_root/aws-cli" \
|
||
--bin-dir "$aws_install_root/bin"
|
||
export PATH="$aws_install_root/bin:$PATH"
|
||
aws --version
|
||
elif command -v brew >/dev/null 2>&1; then
|
||
brew install awscli
|
||
else
|
||
echo "❌ aws CLI missing and no apt-get/brew to install it; cannot upload to R2"
|
||
exit 1
|
||
fi
|
||
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: sm-standard-2
|
||
timeout-minutes: 10
|
||
steps:
|
||
- name: Build completion summary
|
||
shell: bash
|
||
env:
|
||
SHOULD_PUBLISH: ${{ needs.build-check.outputs.should_publish }}
|
||
BUILD_TYPE: ${{ needs.build-check.outputs.build_type }}
|
||
VERSION: ${{ needs.build-check.outputs.version }}
|
||
BUILD_STATUS: ${{ needs.build-rustfs.result }}
|
||
EVENT_NAME: ${{ github.event_name }}
|
||
run: |
|
||
echo "Build type: $BUILD_TYPE"
|
||
echo "Version: $VERSION"
|
||
echo "Build result: $BUILD_STATUS"
|
||
if [[ "$SHOULD_PUBLISH" != "true" ]]; then
|
||
echo "Artifact-only run: no download channels or GitHub Releases are updated."
|
||
echo "Successful platform packages are available as workflow artifacts."
|
||
elif [[ "$BUILD_STATUS" == "success" ]]; then
|
||
echo "Publishing enabled; check the upload and release jobs for their results."
|
||
else
|
||
echo "Build incomplete; check individual platform jobs for any completed uploads."
|
||
fi
|
||
if [[ "$EVENT_NAME" == "push" && "$GITHUB_REF" == refs/tags/* && "$BUILD_STATUS" == "success" ]]; then
|
||
echo "Docker publishing is eligible through the downstream workflow."
|
||
else
|
||
echo "This run does not trigger Docker publishing."
|
||
fi
|
||
|
||
# Create GitHub Release for every valid release tag, including previews
|
||
create-release:
|
||
name: Create GitHub Release
|
||
needs: [ build-check, build-rustfs ]
|
||
if: needs.build-check.outputs.should_publish == 'true' && startsWith(github.ref, 'refs/tags/') && (needs.build-check.outputs.build_type == 'preview' || needs.build-check.outputs.build_type == 'release' || needs.build-check.outputs.build_type == 'prerelease')
|
||
runs-on: ubuntu-latest # gh CLI required; the sm-standard-2 fleet does not ship it
|
||
timeout-minutes: 30
|
||
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@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||
with:
|
||
persist-credentials: false
|
||
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 }}"
|
||
TARGET_COMMITISH=$(git rev-parse --verify "refs/tags/${TAG}^{commit}")
|
||
|
||
# Determine release type for title
|
||
if [[ "$BUILD_TYPE" == "preview" ]]; then
|
||
RELEASE_TYPE="preview"
|
||
elif [[ "$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
|
||
|
||
# Create release title
|
||
if [[ "$IS_PRERELEASE" == "true" ]]; then
|
||
TITLE="RustFS $VERSION (${RELEASE_TYPE})"
|
||
else
|
||
TITLE="RustFS $VERSION"
|
||
fi
|
||
|
||
./scripts/release/create_or_update_release.sh \
|
||
"$TAG" \
|
||
"$TARGET_COMMITISH" \
|
||
"$TITLE" \
|
||
"$IS_PRERELEASE"
|
||
|
||
# Prepare and upload release assets
|
||
upload-release-assets:
|
||
name: Upload Release Assets
|
||
needs: [ build-check, build-rustfs, create-release ]
|
||
if: needs.build-check.outputs.should_publish == 'true' && startsWith(github.ref, 'refs/tags/') && (needs.build-check.outputs.build_type == 'preview' || needs.build-check.outputs.build_type == 'release' || needs.build-check.outputs.build_type == 'prerelease')
|
||
runs-on: ubuntu-latest # gh CLI required; the sm-standard-2 fleet does not ship it
|
||
timeout-minutes: 30
|
||
permissions:
|
||
contents: write
|
||
actions: read
|
||
steps:
|
||
- name: Checkout repository
|
||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||
with:
|
||
persist-credentials: false
|
||
|
||
- name: Download all build artifacts
|
||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||
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 compgen -G "*.zip" >/dev/null; 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 every release tag (stable and prerelease): the
|
||
# project currently ships prerelease tags only, so gating this to stable
|
||
# left the version pointer permanently stale. release_type records whether
|
||
# the pointed-to version is a prerelease.
|
||
update-latest-version:
|
||
name: Update Latest Version
|
||
needs: [ build-check, publish-release ]
|
||
if: needs.build-check.outputs.should_publish == 'true' && startsWith(github.ref, 'refs/tags/') && (needs.build-check.outputs.build_type == 'release' || needs.build-check.outputs.build_type == 'prerelease')
|
||
runs-on: sm-standard-2
|
||
timeout-minutes: 30
|
||
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 }}"
|
||
|
||
if [[ "${{ needs.build-check.outputs.build_type }}" == "prerelease" ]]; then
|
||
RELEASE_TYPE="prerelease"
|
||
else
|
||
RELEASE_TYPE="stable"
|
||
fi
|
||
|
||
# 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": "${RELEASE_TYPE}",
|
||
"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 ${RELEASE_TYPE} release $VERSION"
|
||
|
||
# Publish release (remove draft status)
|
||
publish-release:
|
||
name: Publish Release
|
||
needs: [ build-check, create-release, upload-release-assets ]
|
||
if: needs.build-check.outputs.should_publish == 'true' && startsWith(github.ref, 'refs/tags/') && (needs.build-check.outputs.build_type == 'preview' || needs.build-check.outputs.build_type == 'release' || needs.build-check.outputs.build_type == 'prerelease')
|
||
runs-on: ubuntu-latest # gh CLI required; the sm-standard-2 fleet does not ship it
|
||
timeout-minutes: 30
|
||
permissions:
|
||
contents: write
|
||
steps:
|
||
- name: Publish release
|
||
env:
|
||
GH_TOKEN: ${{ github.token }}
|
||
shell: bash
|
||
run: |
|
||
TAG="${{ needs.build-check.outputs.version }}"
|
||
BUILD_TYPE="${{ needs.build-check.outputs.build_type }}"
|
||
RELEASE_ID="${{ needs.create-release.outputs.release_id }}"
|
||
|
||
# Publish the release and correct its channel state on retries.
|
||
# Only a stable final release may become GitHub Latest.
|
||
if [[ "$BUILD_TYPE" == "release" ]]; then
|
||
gh api --method PATCH "repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}" \
|
||
-F draft=false \
|
||
-F prerelease=false \
|
||
-f make_latest=true >/dev/null
|
||
else
|
||
gh api --method PATCH "repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}" \
|
||
-F draft=false \
|
||
-F prerelease=true \
|
||
-f make_latest=false >/dev/null
|
||
fi
|
||
|
||
echo "🎉 Released $TAG successfully!"
|
||
echo "📄 Release URL: ${{ needs.create-release.outputs.release_url }}"
|
||
|
||
# Remove the internal preview releases once the deliverable release is live.
|
||
# Only the Releases are deleted; the -preview.N tags stay so the validated
|
||
# commit remains traceable.
|
||
cleanup-preview-releases:
|
||
name: Cleanup Preview Releases
|
||
needs: [ build-check, publish-release ]
|
||
if: needs.build-check.outputs.should_publish == 'true' && startsWith(github.ref, 'refs/tags/') && (needs.build-check.outputs.build_type == 'release' || needs.build-check.outputs.build_type == 'prerelease')
|
||
runs-on: ubuntu-latest # gh CLI required; the sm-standard-2 fleet does not ship it
|
||
timeout-minutes: 30
|
||
permissions:
|
||
contents: write
|
||
steps:
|
||
- name: Delete preview releases for this target
|
||
env:
|
||
GH_TOKEN: ${{ github.token }}
|
||
shell: bash
|
||
run: |
|
||
set -euo pipefail
|
||
|
||
TAG="${{ needs.build-check.outputs.version }}"
|
||
RELEASES_JSON="${RUNNER_TEMP}/releases.json"
|
||
|
||
# Fetch before filtering: a failed listing must abort here instead of
|
||
# looking like "nothing to clean up".
|
||
gh api --paginate "repos/${GITHUB_REPOSITORY}/releases?per_page=100" > "$RELEASES_JSON"
|
||
|
||
# Match only <target>-preview.<digits>. String operations, not a
|
||
# regex over the tag, so dots in the version cannot widen the match.
|
||
DELETED=0
|
||
while IFS= read -r preview_tag; do
|
||
[[ -n "$preview_tag" ]] || continue
|
||
echo "🧹 Deleting preview release $preview_tag (tag kept)"
|
||
gh release delete "$preview_tag" --repo "${GITHUB_REPOSITORY}" --yes
|
||
DELETED=$((DELETED + 1))
|
||
done < <(
|
||
jq -r --arg tag "$TAG" '
|
||
.[]
|
||
| select(.tag_name | startswith($tag + "-preview."))
|
||
| select(.tag_name | ltrimstr($tag + "-preview.") | test("^[0-9]+$"))
|
||
| .tag_name
|
||
' "$RELEASES_JSON"
|
||
)
|
||
|
||
if [[ "$DELETED" -eq 0 ]]; then
|
||
echo "ℹ️ No preview releases to clean up for $TAG"
|
||
else
|
||
echo "✅ Removed $DELETED preview release(s) for $TAG"
|
||
fi
|
||
|
||
alert-on-failure:
|
||
name: Alert on scheduled failure
|
||
needs: [build-check, prepare-platform-matrix, build-rustfs, build-summary]
|
||
if: >-
|
||
always() && github.event_name == 'schedule' &&
|
||
(contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled'))
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 10
|
||
permissions:
|
||
contents: read
|
||
issues: write
|
||
steps:
|
||
- uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||
with:
|
||
persist-credentials: false
|
||
- name: Open or update failure-tracking issue
|
||
uses: ./.github/actions/schedule-failure-issue
|
||
with:
|
||
github-token: ${{ secrets.BACKLOG_ISSUE_TOKEN }}
|