diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml new file mode 100644 index 000000000..1dc47b9d9 --- /dev/null +++ b/.github/workflows/package.yml @@ -0,0 +1,463 @@ +# 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. + +# Package Workflow - Build DEB/RPM packages +# +# This workflow builds DEB and RPM packages from pre-built Linux binaries +# and uploads them to Cloudflare R2. +# +# Trigger: +# - release published: automatically package when a GitHub release is published +# - workflow_dispatch: manual trigger with optional tag/run_id +# +# Flow: +# 1. Find the Build workflow run for the release tag +# 2. Download Linux binaries (x86_64-gnu, aarch64-gnu) from build artifacts +# 3. Build DEB packages for amd64 and arm64 +# 4. Build RPM packages for x86_64 and aarch64 +# 5. Upload all packages to Cloudflare R2 + +name: Package DEB/RPM + +permissions: + contents: read + actions: read + +on: + release: + types: [ published ] + workflow_dispatch: + inputs: + tag: + description: "Release tag to package (e.g. 1.0.0-beta.12). Leave empty for latest main build." + required: false + type: string + build_run_id: + description: "Build workflow run ID (overrides tag lookup)" + required: false + type: string + +concurrency: + group: ${{ github.workflow }}-${{ github.event.release.tag_name || github.event.inputs.tag || github.run_id }} + cancel-in-progress: true + +jobs: + # Resolve which build run to use and extract version info + resolve: + name: Resolve Build + runs-on: ubuntu-latest + timeout-minutes: 10 + outputs: + version: ${{ steps.resolve.outputs.version }} + build_type: ${{ steps.resolve.outputs.build_type }} + build_run_id: ${{ steps.resolve.outputs.build_run_id }} + tag: ${{ steps.resolve.outputs.tag }} + steps: + - name: Resolve build run + id: resolve + shell: bash + env: + GH_TOKEN: ${{ github.token }} + INPUT_TAG: ${{ github.event.inputs.tag }} + INPUT_RUN_ID: ${{ github.event.inputs.build_run_id }} + run: | + set -euo pipefail + + # Determine tag + if [[ "${{ github.event_name }}" == "release" ]]; then + TAG="${{ github.event.release.tag_name }}" + elif [[ -n "$INPUT_TAG" ]]; then + TAG="$INPUT_TAG" + else + TAG="" + fi + + echo "Tag: ${TAG:-}" + + # Determine build run ID + BUILD_RUN_ID="" + + if [[ -n "$INPUT_RUN_ID" ]]; then + # Explicit run ID takes priority + BUILD_RUN_ID="$INPUT_RUN_ID" + echo "Using explicit build run ID: $BUILD_RUN_ID" + + elif [[ -n "$TAG" ]]; then + # Find the build run that produced this tag + echo "Looking for build run for tag: $TAG" + BUILD_RUN_ID=$(gh api \ + "repos/${{ github.repository }}/actions/workflows/build.yml/runs?branch=${TAG}&status=success&per_page=1" \ + --jq '.workflow_runs[0].id' 2>/dev/null || echo "") + + if [[ -z "$BUILD_RUN_ID" || "$BUILD_RUN_ID" == "null" ]]; then + # Tag might not be a branch; try event=push with head_branch matching + BUILD_RUN_ID=$(gh api \ + "repos/${{ github.repository }}/actions/workflows/build.yml/runs?event=push&status=success&per_page=100" \ + --jq ".workflow_runs[] | select(.head_branch == \"$TAG\") | .id" 2>/dev/null | head -1 || echo "") + fi + + if [[ -z "$BUILD_RUN_ID" || "$BUILD_RUN_ID" == "null" ]]; then + echo "❌ No successful build run found for tag: $TAG" + exit 1 + fi + echo "Found build run: $BUILD_RUN_ID" + + else + # No tag — latest successful main build + echo "No tag specified, looking for latest main build" + BUILD_RUN_ID=$(gh api \ + "repos/${{ github.repository }}/actions/workflows/build.yml/runs?branch=main&status=success&per_page=1" \ + --jq '.workflow_runs[0].id' 2>/dev/null || echo "") + + if [[ -z "$BUILD_RUN_ID" || "$BUILD_RUN_ID" == "null" ]]; then + echo "❌ No successful main build found" + exit 1 + fi + echo "Latest main build: $BUILD_RUN_ID" + fi + + # Determine version and build type + if [[ -n "$TAG" ]]; then + VERSION="$TAG" + if [[ "$TAG" == *"-preview"* ]]; then + BUILD_TYPE="preview" + elif [[ "$TAG" == *"alpha"* || "$TAG" == *"beta"* || "$TAG" == *"rc"* ]]; then + BUILD_TYPE="prerelease" + else + BUILD_TYPE="release" + fi + else + SHORT_SHA=$(gh api "repos/${{ github.repository }}/actions/runs/${BUILD_RUN_ID}" \ + --jq '.head_sha' 2>/dev/null | head -c 7) + VERSION="dev-${SHORT_SHA}" + BUILD_TYPE="development" + fi + + { + echo "version=$VERSION" + echo "build_type=$BUILD_TYPE" + echo "build_run_id=$BUILD_RUN_ID" + echo "tag=${TAG}" + } >> "$GITHUB_OUTPUT" + + echo "📊 Resolved:" + echo " Version: $VERSION" + echo " Build type: $BUILD_TYPE" + echo " Build run ID: $BUILD_RUN_ID" + + # Build DEB and RPM packages for each architecture + package: + name: Package (${{ matrix.arch }}) + needs: resolve + runs-on: ubuntu-latest + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + include: + - arch: x86_64 + deb_arch: amd64 + rpm_arch: x86_64 + artifact_name: "rustfs-linux-x86_64-gnu" + - arch: aarch64 + deb_arch: arm64 + rpm_arch: aarch64 + artifact_name: "rustfs-linux-aarch64-gnu" + steps: + - name: Checkout repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + persist-credentials: false + + - name: Download binary artifact from build run + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 + with: + pattern: ${{ matrix.artifact_name }}* + path: ./binary-artifact + run-id: ${{ needs.resolve.outputs.build_run_id }} + github-token: ${{ github.token }} + merge-multiple: true + + - name: Extract binary + id: binary + shell: bash + run: | + set -euo pipefail + + ZIP_FILE=$(find ./binary-artifact -name "*.zip" -type f | head -1) + if [[ -z "$ZIP_FILE" ]]; then + echo "❌ No binary artifact found" + ls -la ./binary-artifact/ || true + exit 1 + fi + + echo "Found artifact: $ZIP_FILE" + + mkdir -p ./bin + unzip -o "$ZIP_FILE" -d ./bin + + if [[ ! -f ./bin/rustfs ]]; then + echo "❌ rustfs binary not found in archive" + exit 1 + fi + + chmod +x ./bin/rustfs + ls -lh ./bin/rustfs + echo "✅ Binary extracted" + + - name: Build DEB package + id: deb + shell: bash + run: | + set -euo pipefail + + VERSION="${{ needs.resolve.outputs.version }}" + DEB_ARCH="${{ matrix.deb_arch }}" + # DEB version: replace - with ~ (1.0.0-beta.12 -> 1.0.0~beta.12) + DEB_VERSION="${VERSION/-/~}" + PKG_DIR="rustfs_${DEB_VERSION}_${DEB_ARCH}" + + echo "Building DEB: ${PKG_DIR}.deb" + + mkdir -p "${PKG_DIR}/DEBIAN" + mkdir -p "${PKG_DIR}/usr/bin" + mkdir -p "${PKG_DIR}/etc/default" + mkdir -p "${PKG_DIR}/lib/systemd/system" + mkdir -p "${PKG_DIR}/usr/share/doc/rustfs" + + cp ./bin/rustfs "${PKG_DIR}/usr/bin/" + chmod 755 "${PKG_DIR}/usr/bin/rustfs" + + cp deploy/build/rustfs.service "${PKG_DIR}/lib/systemd/system/" + + cat > "${PKG_DIR}/etc/default/rustfs" << 'ENVEOF' + # RustFS Environment Configuration + # See https://rustfs.com/docs/ for more information + # RUSTFS_VOLUMES="" + # RUSTFS_ROOT_USER="" + # RUSTFS_ROOT_PASSWORD="" + ENVEOF + + cat > "${PKG_DIR}/DEBIAN/control" << EOF + Package: rustfs + Version: ${DEB_VERSION} + Section: utils + Priority: optional + Architecture: ${DEB_ARCH} + Depends: libc6 (>= 2.31) + Maintainer: RustFS Team + Description: High-performance distributed object storage + RustFS is a high-performance distributed object storage software + built using Rust. It is compatible with MinIO and S3 API. + Homepage: https://rustfs.com + EOF + + cat > "${PKG_DIR}/DEBIAN/postinst" << 'POSTINST' + #!/bin/bash + set -e + if ! getent passwd rustfs > /dev/null 2>&1; then + useradd -r -s /bin/false -d /opt/rustfs rustfs + fi + mkdir -p /opt/rustfs /data/rustfs /var/log/rustfs + chown rustfs:rustfs /opt/rustfs /data/rustfs /var/log/rustfs + if [ -d /run/systemd/system ]; then + systemctl daemon-reload + fi + echo "RustFS installed. Configure /etc/default/rustfs then: systemctl start rustfs" + POSTINST + chmod 755 "${PKG_DIR}/DEBIAN/postinst" + + cat > "${PKG_DIR}/DEBIAN/prerm" << 'PRERM' + #!/bin/bash + set -e + if [ -d /run/systemd/system ] && systemctl is-active --quiet rustfs; then + systemctl stop rustfs + fi + PRERM + chmod 755 "${PKG_DIR}/DEBIAN/prerm" + + cat > "${PKG_DIR}/DEBIAN/postrm" << 'POSTRM' + #!/bin/bash + set -e + if [ -d /run/systemd/system ]; then + systemctl daemon-reload + fi + POSTRM + chmod 755 "${PKG_DIR}/DEBIAN/postrm" + + cp LICENSE "${PKG_DIR}/usr/share/doc/rustfs/" + cp README.md "${PKG_DIR}/usr/share/doc/rustfs/" + + fakeroot dpkg-deb --build "${PKG_DIR}" + + DEB_FILE="${PKG_DIR}.deb" + ls -lh "$DEB_FILE" + echo "deb_file=$DEB_FILE" >> "$GITHUB_OUTPUT" + echo "✅ DEB built: $DEB_FILE" + + - name: Build RPM package + id: rpm + shell: bash + run: | + set -euo pipefail + + VERSION="${{ needs.resolve.outputs.version }}" + RPM_ARCH="${{ matrix.rpm_arch }}" + + echo "Building RPM for ${RPM_ARCH}" + + sudo apt-get update && sudo apt-get install -y ruby ruby-dev build-essential + sudo gem install fpm + + fpm -s dir -t rpm \ + --name rustfs \ + --version "$VERSION" \ + --architecture "$RPM_ARCH" \ + --depends "glibc >= 2.31" \ + --maintainer "RustFS Team " \ + --description "High-performance distributed object storage" \ + --url "https://rustfs.com" \ + --license "Apache-2.0" \ + --after-install <(cat <<'POSTINST' + #!/bin/bash + set -e + if ! getent passwd rustfs > /dev/null 2>&1; then + useradd -r -s /bin/false -d /opt/rustfs rustfs + fi + mkdir -p /opt/rustfs /data/rustfs /var/log/rustfs + chown rustfs:rustfs /opt/rustfs /data/rustfs /var/log/rustfs + if [ -d /run/systemd/system ]; then + systemctl daemon-reload + fi + POSTINST + ) \ + --before-remove <(cat <<'PRERM' + #!/bin/bash + set -e + if [ -d /run/systemd/system ] && systemctl is-active --quiet rustfs; then + systemctl stop rustfs + fi + PRERM + ) \ + --after-remove <(cat <<'POSTRM' + #!/bin/bash + set -e + if [ -d /run/systemd/system ]; then + systemctl daemon-reload + fi + POSTRM + ) \ + --config-files /etc/default/rustfs \ + ./bin/rustfs=/usr/bin/rustfs \ + deploy/build/rustfs.service=/lib/systemd/system/rustfs.service \ + LICENSE=/usr/share/doc/rustfs/LICENSE \ + README.md=/usr/share/doc/rustfs/README.md + + RPM_FILE=$(ls -1 rustfs-*.rpm 2>/dev/null | head -1) + if [[ -z "$RPM_FILE" ]]; then + echo "❌ RPM build failed" + exit 1 + fi + + ls -lh "$RPM_FILE" + echo "rpm_file=$RPM_FILE" >> "$GITHUB_OUTPUT" + echo "✅ RPM built: $RPM_FILE" + + - name: Upload packages to artifacts + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + with: + name: packages-${{ matrix.arch }} + path: | + *.deb + *.rpm + retention-days: 30 + + - name: Upload packages to Cloudflare R2 + if: env.R2_ACCESS_KEY_ID != '' + 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: | + set -euo pipefail + + if [[ -z "$R2_ACCESS_KEY_ID" || -z "$R2_SECRET_ACCESS_KEY" || -z "$R2_ENDPOINT" || -z "$R2_BUCKET" ]]; then + echo "⚠️ R2 credentials missing, skipping upload" + exit 0 + fi + + if ! command -v aws >/dev/null 2>&1; then + sudo apt-get update && sudo apt-get install -y awscli + fi + + export AWS_ACCESS_KEY_ID="$R2_ACCESS_KEY_ID" + export AWS_SECRET_ACCESS_KEY="$R2_SECRET_ACCESS_KEY" + export AWS_DEFAULT_REGION="auto" + + BUILD_TYPE="${{ needs.resolve.outputs.build_type }}" + if [[ "$BUILD_TYPE" == "development" ]]; then + R2_PREFIX="artifacts/rustfs/packages/dev" + else + R2_PREFIX="artifacts/rustfs/packages/release" + fi + R2_PATH="s3://${R2_BUCKET}/${R2_PREFIX}/" + + echo "📤 Uploading to $R2_PATH" + + DEB_FILE="${{ steps.deb.outputs.deb_file }}" + RPM_FILE="${{ steps.rpm.outputs.rpm_file }}" + + for f in "$DEB_FILE" "$RPM_FILE"; do + if [[ -n "$f" && -f "$f" ]]; then + echo "Uploading: $f" + aws s3 cp "$f" "$R2_PATH" --endpoint-url "$R2_ENDPOINT" --only-show-errors + fi + done + + echo "✅ Upload complete" + + # Also upload as latest for release/prerelease + if [[ "$BUILD_TYPE" == "release" || "$BUILD_TYPE" == "prerelease" ]]; then + LATEST_PATH="s3://${R2_BUCKET}/artifacts/rustfs/packages/latest/" + for f in "$DEB_FILE" "$RPM_FILE"; do + if [[ -n "$f" && -f "$f" ]]; then + echo "Uploading latest: $(basename "$f")" + aws s3 cp "$f" "$LATEST_PATH" --endpoint-url "$R2_ENDPOINT" --only-show-errors + fi + done + echo "✅ Latest packages updated" + fi + + # Summary + summary: + name: Summary + needs: [ resolve, package ] + if: always() + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Print summary + shell: bash + run: | + echo "## 📦 Package Summary" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "| Item | Value |" >> "$GITHUB_STEP_SUMMARY" + echo "|------|-------|" >> "$GITHUB_STEP_SUMMARY" + echo "| Version | \`${{ needs.resolve.outputs.version }}\` |" >> "$GITHUB_STEP_SUMMARY" + echo "| Build Type | ${{ needs.resolve.outputs.build_type }} |" >> "$GITHUB_STEP_SUMMARY" + echo "| Build Run | #${{ needs.resolve.outputs.build_run_id }} |" >> "$GITHUB_STEP_SUMMARY" + echo "| Package Status | ${{ needs.package.result }} |" >> "$GITHUB_STEP_SUMMARY" diff --git a/deploy/build/rustfs.service b/deploy/build/rustfs.service index 748067045..08846ce64 100644 --- a/deploy/build/rustfs.service +++ b/deploy/build/rustfs.service @@ -20,7 +20,7 @@ WorkingDirectory=/opt/rustfs # rustfs reads address, volumes, console, credentials, and other runtime settings # from RUSTFS_* variables. See `../config/rustfs.env` for an example template. EnvironmentFile=/etc/default/rustfs -ExecStart=/usr/local/bin/rustfs server +ExecStart=/usr/bin/rustfs server # service log configuration LogsDirectory=rustfs