Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
359 changes: 359 additions & 0 deletions .github/workflows/package.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,359 @@
name: Package DEB/RPM

permissions:
contents: write
actions: read

on:
workflow_run:
workflows: ["Build and Release"]
types: [completed]
workflow_dispatch:
inputs:
tag:
description: "Release tag to package (for example v0.1.24 or v0.1.24-rc.1)"
required: false
type: string
build_run_id:
description: "Build and Release workflow run ID to package"
required: false
type: string

concurrency:
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch || github.event.inputs.tag || github.run_id }}
cancel-in-progress: true

env:
HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
WORKFLOW_RUN_ID: ${{ github.event.workflow_run.id }}

jobs:
resolve:
name: Resolve Build
if: >-
github.event_name == 'workflow_dispatch' ||
(github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'push' &&
github.event.workflow_run.head_branch != 'main')
runs-on: ubuntu-latest
timeout-minutes: 10
outputs:
version: ${{ steps.resolve.outputs.version }}
package_version: ${{ steps.resolve.outputs.package_version }}
build_run_id: ${{ steps.resolve.outputs.build_run_id }}
tag: ${{ steps.resolve.outputs.tag }}
steps:
- name: Resolve build run
id: resolve
env:
GH_TOKEN: ${{ github.token }}
INPUT_TAG: ${{ github.event.inputs.tag }}
INPUT_RUN_ID: ${{ github.event.inputs.build_run_id }}
shell: bash
run: |
set -euo pipefail

if [[ "${{ github.event_name }}" == "workflow_run" ]]; then
TAG="${HEAD_BRANCH}"
elif [[ -n "${INPUT_TAG}" ]]; then
TAG="${INPUT_TAG}"
else
TAG=""
fi

BUILD_RUN_ID=""
if [[ -n "${INPUT_RUN_ID}" ]]; then
BUILD_RUN_ID="${INPUT_RUN_ID}"
echo "Using explicit build run ID: ${BUILD_RUN_ID}"
elif [[ "${{ github.event_name }}" == "workflow_run" ]]; then
BUILD_RUN_ID="${WORKFLOW_RUN_ID}"
echo "Using triggering workflow run: ${BUILD_RUN_ID}"
elif [[ -n "${TAG}" ]]; then
echo "Looking for release workflow run for tag: ${TAG}"
BUILD_RUN_ID=$(gh api \
"repos/${{ github.repository }}/actions/workflows/release.yml/runs?event=push&status=success&per_page=100" \
--jq ".workflow_runs[] | select(.head_branch == \"${TAG}\") | .id" 2>/dev/null | head -1 || echo "")

if [[ -z "${BUILD_RUN_ID}" || "${BUILD_RUN_ID}" == "null" ]]; then
echo "No successful release workflow run found for tag: ${TAG}"
exit 1
fi
else
echo "A tag or build_run_id is required for manual packaging"
exit 1
fi

if [[ -z "${TAG}" ]]; then
TAG=$(gh api "repos/${{ github.repository }}/actions/runs/${BUILD_RUN_ID}" --jq '.head_branch' 2>/dev/null || echo "")
fi

if [[ -z "${TAG}" || "${TAG}" == "null" ]]; then
echo "Unable to resolve release tag for workflow run ${BUILD_RUN_ID}"
exit 1
fi

if [[ ! "${TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z.]+)?$ ]]; then
echo "Resolved tag is not a release tag: ${TAG}"
exit 1
fi

PACKAGE_VERSION="${TAG#v}"
PACKAGE_VERSION="${PACKAGE_VERSION/-/~}"

{
echo "version=${TAG}"
echo "package_version=${PACKAGE_VERSION}"
echo "build_run_id=${BUILD_RUN_ID}"
echo "tag=${TAG}"
} >> "$GITHUB_OUTPUT"

echo "Resolved version: ${TAG}"
echo "Resolved package version: ${PACKAGE_VERSION}"
echo "Resolved workflow run: ${BUILD_RUN_ID}"

package:
name: Package (${{ matrix.arch }})
needs: resolve
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
include:
- arch: amd64
rpm_arch: x86_64
artifact_name: rustfs-cli-linux-amd64-gnu
- arch: arm64
rpm_arch: aarch64
artifact_name: rustfs-cli-linux-arm64-gnu
steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ needs.resolve.outputs.tag }}

- name: Download binary artifact from release build
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: Download completions artifact
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
name: completions
path: ./completions-artifact
run-id: ${{ needs.resolve.outputs.build_run_id }}
github-token: ${{ github.token }}

- name: Extract release assets
shell: bash
run: |
set -euo pipefail

TAR_FILE=$(find ./binary-artifact -name '*.tar.gz' -type f | head -1)
if [[ -z "${TAR_FILE}" ]]; then
echo "No Linux binary archive found"
ls -la ./binary-artifact/ || true
exit 1
fi

mkdir -p ./binary-extract
tar -xzf "${TAR_FILE}" -C ./binary-extract

mkdir -p ./pkg-root/usr/bin
install -m 755 ./binary-extract/rc ./pkg-root/usr/bin/rc
chmod 755 ./pkg-root/usr/bin/rc

mkdir -p ./pkg-root/usr/share/doc/rustfs-cli
cp LICENSE-MIT LICENSE-APACHE README.md ./pkg-root/usr/share/doc/rustfs-cli/

mkdir -p ./pkg-root/usr/share/bash-completion/completions
mkdir -p ./pkg-root/usr/share/zsh/site-functions
mkdir -p ./pkg-root/usr/share/fish/vendor_completions.d

if [[ ! -f ./completions-artifact/completions.tar.gz ]]; then
echo "No completions archive found"
ls -la ./completions-artifact/ || true
exit 1
fi

tar -xzf ./completions-artifact/completions.tar.gz -C ./completions-artifact
cp ./completions-artifact/completions/rc.bash ./pkg-root/usr/share/bash-completion/completions/rc
cp ./completions-artifact/completions/_rc ./pkg-root/usr/share/zsh/site-functions/_rc
cp ./completions-artifact/completions/rc.fish ./pkg-root/usr/share/fish/vendor_completions.d/rc.fish

- name: Install packaging tools
shell: bash
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get install -y fakeroot ruby ruby-dev build-essential rpm
sudo gem install --no-document fpm

- name: Build DEB package
id: deb
shell: bash
run: |
set -euo pipefail

VERSION="${{ needs.resolve.outputs.package_version }}"
PKG_DIR="rustfs-cli_${VERSION}_${{ matrix.arch }}"

mkdir -p "${PKG_DIR}/DEBIAN"
cp -R ./pkg-root/. "${PKG_DIR}/"

cat > "${PKG_DIR}/DEBIAN/control" << EOF
Package: rustfs-cli
Version: ${VERSION}
Section: utils
Priority: optional
Architecture: ${{ matrix.arch }}
Depends: libc6 (>= 2.31)
Maintainer: RustFS Team <support@rustfs.com>
Description: Rust S3 CLI client for S3-compatible object storage
rc is a command-line client for RustFS, MinIO, AWS S3,
and other S3-compatible object storage services.
Homepage: https://github.com/rustfs/cli
EOF

fakeroot dpkg-deb --build "${PKG_DIR}"

DEB_FILE="${PKG_DIR}.deb"
echo "deb_file=${DEB_FILE}" >> "$GITHUB_OUTPUT"
ls -lh "${DEB_FILE}"

- name: Build RPM package
id: rpm
shell: bash
run: |
set -euo pipefail

VERSION="${{ needs.resolve.outputs.package_version }}"
fpm -s dir -t rpm \
--name rustfs-cli \
--version "${VERSION}" \
--iteration 1 \
--architecture "${{ matrix.rpm_arch }}" \
--depends 'glibc >= 2.31' \
--maintainer 'RustFS Team <support@rustfs.com>' \
--description 'Rust S3 CLI client for S3-compatible object storage' \
--url 'https://github.com/rustfs/cli' \
--license 'MIT OR Apache-2.0' \
./pkg-root/usr/bin/rc=/usr/bin/rc \
./pkg-root/usr/share/doc/rustfs-cli/LICENSE-MIT=/usr/share/doc/rustfs-cli/LICENSE-MIT \
./pkg-root/usr/share/doc/rustfs-cli/LICENSE-APACHE=/usr/share/doc/rustfs-cli/LICENSE-APACHE \
./pkg-root/usr/share/doc/rustfs-cli/README.md=/usr/share/doc/rustfs-cli/README.md \
./pkg-root/usr/share/bash-completion/completions/rc=/usr/share/bash-completion/completions/rc \
./pkg-root/usr/share/zsh/site-functions/_rc=/usr/share/zsh/site-functions/_rc \
./pkg-root/usr/share/fish/vendor_completions.d/rc.fish=/usr/share/fish/vendor_completions.d/rc.fish

RPM_FILE=$(ls -1 rustfs-cli-*.rpm 2>/dev/null | head -1)
if [[ -z "${RPM_FILE}" ]]; then
echo "RPM build failed"
exit 1
fi

echo "rpm_file=${RPM_FILE}" >> "$GITHUB_OUTPUT"
ls -lh "${RPM_FILE}"

- name: Upload package artifacts
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: packages-${{ matrix.arch }}
path: |
*.deb
*.rpm
retention-days: 30

- name: Upload packages to Cloudflare R2
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"
export AWS_REQUEST_CHECKSUM_CALCULATION="when_required"
export AWS_RESPONSE_CHECKSUM_VALIDATION="when_required"

VERSION="${{ needs.resolve.outputs.version }}"
VERSION_PATH="s3://${R2_BUCKET}/artifacts/rustfs-cli/packages/release/${VERSION}/"
LATEST_PATH="s3://${R2_BUCKET}/artifacts/rustfs-cli/packages/latest/"

for file in "${{ steps.deb.outputs.deb_file }}" "${{ steps.rpm.outputs.rpm_file }}"; do
if [[ -n "${file}" && -f "${file}" ]]; then
aws s3 cp "${file}" "${VERSION_PATH}" --endpoint-url "${R2_ENDPOINT}" --only-show-errors
aws s3 cp "${file}" "${LATEST_PATH}" --endpoint-url "${R2_ENDPOINT}" --only-show-errors
fi
done

- name: Upload packages to GitHub Release
if: needs.resolve.outputs.tag != ''
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
set -euo pipefail

TAG="${{ needs.resolve.outputs.tag }}"
CHECKSUM_FILE="$(mktemp)"
gh release download "${TAG}" -p 'SHA256SUMS' -D "$(dirname "${CHECKSUM_FILE}")" --clobber 2>/dev/null || true
if [[ -f "$(dirname "${CHECKSUM_FILE}")/SHA256SUMS" ]]; then
mv "$(dirname "${CHECKSUM_FILE}")/SHA256SUMS" "${CHECKSUM_FILE}"
else
: > "${CHECKSUM_FILE}"
fi

for file in "${{ steps.deb.outputs.deb_file }}" "${{ steps.rpm.outputs.rpm_file }}"; do
if [[ -n "${file}" && -f "${file}" ]]; then
base=$(basename "${file}")
github_base="${base//\~/.}"
gh release upload "${TAG}" "${file}" --clobber
grep -Fv -- "${base}" "${CHECKSUM_FILE}" > "${CHECKSUM_FILE}.tmp" || true
grep -Fv -- "${github_base}" "${CHECKSUM_FILE}.tmp" > "${CHECKSUM_FILE}.next" || true
mv "${CHECKSUM_FILE}.next" "${CHECKSUM_FILE}"
checksum=$(sha256sum "${file}" | awk '{print $1}')
printf '%s %s\n' "${checksum}" "${github_base}" >> "${CHECKSUM_FILE}"
fi
done

mv "${CHECKSUM_FILE}" "$(dirname "${CHECKSUM_FILE}")/SHA256SUMS"
gh release upload "${TAG}" "$(dirname "${CHECKSUM_FILE}")/SHA256SUMS" --clobber

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 Run | #${{ needs.resolve.outputs.build_run_id }} |" >> "$GITHUB_STEP_SUMMARY"
echo "| Package Status | ${{ needs.package.result }} |" >> "$GITHUB_STEP_SUMMARY"
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ Download the appropriate binary for your platform from the [Releases](https://gi
On Linux, use the default `linux-amd64` / `linux-arm64` artifacts for maximum compatibility (`musl` static build).
If you specifically need glibc-linked builds, use `linux-amd64-gnu` / `linux-arm64-gnu`.

### DEB/RPM Packages

Debian and RPM packages are published as GitHub release assets for each tagged release.
Download the appropriate `.deb` or `.rpm` package from the [Releases](https://github.com/rustfs/cli/releases) page and install it with your system package manager.

### Homebrew (macOS/Linux)

```bash
Expand Down
2 changes: 1 addition & 1 deletion crates/cli/src/commands/anonymous.rs
Original file line number Diff line number Diff line change
Expand Up @@ -675,7 +675,7 @@ fn build_policy(level: &AccessLevel, bucket: &str, prefix: &str) -> String {
let sanitized = prefix.trim_end_matches('/');
condition = serde_json::json!({
"StringLike": {
"s3:prefix": [format!("{sanitized}"), format!("{sanitized}/*"), format!("{sanitized}*")]
"s3:prefix": [sanitized.to_string(), format!("{sanitized}/*"), format!("{sanitized}*")]
}
});
}
Expand Down
Loading