diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 675a636179..8328572ff1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,6 +25,7 @@ concurrency: jobs: release: runs-on: [ "self-hosted", "ragflow-release" ] + steps: - name: Ensure workspace ownership run: echo "chown -R ${USER} ${GITHUB_WORKSPACE}" && sudo chown -R ${USER} ${GITHUB_WORKSPACE} @@ -37,6 +38,13 @@ jobs: fetch-depth: 0 fetch-tags: true + # https://github.com/actions/setup-go + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + - name: Prepare release body run: | if [[ ${GITHUB_EVENT_NAME} != "schedule" ]]; then @@ -52,8 +60,10 @@ jobs: PRERELEASE=true echo "Workflow triggered by schedule" fi + echo "RELEASE_TAG=${RELEASE_TAG}" >> ${GITHUB_ENV} echo "PRERELEASE=${PRERELEASE}" >> ${GITHUB_ENV} + RELEASE_DATETIME=$(date --rfc-3339=seconds) echo Release ${RELEASE_TAG} created from ${GITHUB_SHA} at ${RELEASE_DATETIME} > release_body.md @@ -73,15 +83,88 @@ jobs: fi fi - - name: Create or overwrite a release - # https://github.com/actions/upload-release-asset has been replaced by https://github.com/softprops/action-gh-release + - name: Build Go CLI release binaries + run: | + set -euo pipefail + + CLI_NAME="ragflow_cli" + + CLI_MAIN="./cmd/ragflow_cli.go" + + DIST_DIR="dist/cli" + + mkdir -p "${DIST_DIR}" + + if [[ ! -e "${CLI_MAIN}" ]]; then + echo "::error::Go CLI entry does not exist: ${CLI_MAIN}" + echo "::error::Please update CLI_MAIN in .github/workflows/release.yml" + exit 1 + fi + + echo "Building Go CLI release binaries" + echo "CLI name: ${CLI_NAME}" + echo "CLI main: ${CLI_MAIN}" + echo "Release tag: ${RELEASE_TAG}" + echo "Commit: ${GITHUB_SHA}" + + build_one() { + local goos="$1" + local goarch="$2" + local ext="$3" + + local output="${DIST_DIR}/${CLI_NAME}-${RELEASE_TAG}-${goos}-${goarch}${ext}" + + echo "Building ${goos}/${goarch} -> ${output}" + + CGO_ENABLED=0 \ + GOOS="${goos}" \ + GOARCH="${goarch}" \ + go build \ + -trimpath \ + -ldflags="-s -w -X main.version=${RELEASE_TAG} -X main.commit=${GITHUB_SHA}" \ + -o "${output}" \ + "${CLI_MAIN}" + + if [[ "${goos}" != "windows" ]]; then + chmod +x "${output}" + fi + } + + build_one linux amd64 "" + build_one linux arm64 "" + build_one darwin amd64 "" + build_one darwin arm64 "" + build_one windows amd64 ".exe" + build_one windows arm64 ".exe" + + if command -v ldd >/dev/null 2>&1; then + if ldd "${DIST_DIR}/${CLI_NAME}-${RELEASE_TAG}-linux-amd64" 2>&1 | grep -q "not a dynamic executable"; then + echo "Verified linux/amd64 CLI is not dynamically linked" + else + echo "::error::linux/amd64 CLI is dynamically linked" + ldd "${DIST_DIR}/${CLI_NAME}-${RELEASE_TAG}-linux-amd64" || true + exit 1 + fi + fi + + cd "${DIST_DIR}" + sha256sum * > SHA256SUMS + cd - + + echo "Generated CLI release assets:" + ls -lh "${DIST_DIR}" + + - name: Upload Go CLI release assets uses: softprops/action-gh-release@v2 with: - token: ${{ secrets.GITHUB_TOKEN }} # Use the secret as an environment variable + token: ${{ secrets.GITHUB_TOKEN }} prerelease: ${{ env.PRERELEASE }} tag_name: ${{ env.RELEASE_TAG }} - # The body field does not support environment variable substitution directly. body_path: release_body.md + files: | + dist/cli/* + install.sh + install.ps1 - name: Build and push image run: | diff --git a/tools/scripts/INSTALL_SCRIPTS_README.md b/tools/scripts/INSTALL_SCRIPTS_README.md new file mode 100644 index 0000000000..7a37d04202 --- /dev/null +++ b/tools/scripts/INSTALL_SCRIPTS_README.md @@ -0,0 +1,99 @@ +# RAGFlow CLI Installation Scripts + +RAGFlow publishes static Go CLI binaries as GitHub Release assets for: + +- Linux: `amd64`, `arm64` +- macOS: `amd64`, `arm64` +- Windows: `amd64`, `arm64` + +The release workflow builds the binaries with `CGO_ENABLED=0`, uploads `SHA256SUMS`, and uploads `install.sh` and `install.ps1`. + +## Linux And macOS + +Use the hosted script: + +```sh +curl -sSfL https://your-domain/install.sh | sh +``` + +Install a specific version: + +```sh +curl -sSfL https://your-domain/install.sh | VERSION=v1.0.0 sh +``` + +Install to a user-writable directory: + +```sh +curl -sSfL https://your-domain/install.sh | INSTALL_DIR="$HOME/.local/bin" sh +``` + +The Unix installer: + +- detects `linux` or `darwin` +- detects `amd64` or `arm64` +- downloads `ragflow_cli-{VERSION}-{OS}-{ARCH}` +- verifies the file with the release `SHA256SUMS` +- installs to `/usr/local/bin` by default + +## Windows PowerShell + +Use the hosted script: + +```powershell +iwr https://your-domain/install.ps1 -OutFile install.ps1 +powershell -ExecutionPolicy Bypass -File .\install.ps1 +``` +Install a specific version: + +```powershell +.\install.ps1 -Version "v1.0.0" +``` + +Install to a custom directory: + +```powershell +.\install.ps1 -InstallDir "$env:USERPROFILE\bin" +``` + +The Windows installer: + +- detects `windows/amd64` or `windows/arm64` +- downloads `ragflow_cli-{VERSION}-windows-{ARCH}.exe` +- verifies the file with the release `SHA256SUMS` +- installs to `$env:LOCALAPPDATA\Programs\RAGFlow` by default +- adds the install directory to the user `PATH` + +## Release Asset Names + +The install scripts expect these names: + +```text +ragflow_cli-v1.0.0-linux-amd64 +ragflow_cli-v1.0.0-linux-arm64 +ragflow_cli-v1.0.0-darwin-amd64 +ragflow_cli-v1.0.0-darwin-arm64 +ragflow_cli-v1.0.0-windows-amd64.exe +ragflow_cli-v1.0.0-windows-arm64.exe +SHA256SUMS +install.sh +install.ps1 +``` + +## Hosting Options + +Use a static domain for the public one-line command: + +```text +https://your-domain/install.sh +https://your-domain/install.ps1 +``` + +The same scripts can also be used directly from a GitHub Release asset, for example: + +```text +https://github.com/infiniflow/ragflow/releases/download/v1.0.0/install.sh +https://github.com/infiniflow/ragflow/releases/download/v1.0.0/install.ps1 +``` + +By default, both scripts install the latest stable GitHub Release. Set `VERSION` on Unix or `-Version` on Windows to pin a specific release. diff --git a/tools/scripts/install.ps1 b/tools/scripts/install.ps1 new file mode 100644 index 0000000000..b7b6baacd3 --- /dev/null +++ b/tools/scripts/install.ps1 @@ -0,0 +1,232 @@ +# +# Copyright 2026 The InfiniFlow Authors. All Rights Reserved. +# +# 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. +# + +param( + [string]$Version = "latest", + [string]$InstallDir = "$env:LOCALAPPDATA\Programs\RAGFlow", + [string]$GitHubRepo = "infiniflow/ragflow", + [string]$CliName = "ragflow_cli" +) + +$ErrorActionPreference = "Stop" + +function Write-Info { + param([string]$Message) + Write-Host "[INFO] $Message" -ForegroundColor Green +} + +function Write-Warn { + param([string]$Message) + Write-Host "[WARN] $Message" -ForegroundColor Yellow +} + +function Write-ErrorMessage { + param([string]$Message) + Write-Host "[ERROR] $Message" -ForegroundColor Red +} + +function Get-Platform { + $os = "windows" + $arch = "amd64" + + if ($env:PROCESSOR_ARCHITECTURE -eq "ARM64" -or $env:PROCESSOR_ARCHITEW6432 -eq "ARM64") { + $arch = "arm64" + } + elseif ([Environment]::Is64BitOperatingSystem) { + $arch = "amd64" + } + else { + Write-ErrorMessage "Unsupported Windows architecture: 386" + exit 1 + } + + Write-Info "Detected platform: ${os}/${arch}" + + return @{ + OS = $os + Arch = $arch + } +} + +function Get-ReleaseVersion { + param( + [string]$RequestedVersion, + [string]$Repository + ) + + if ($RequestedVersion -ne "latest") { + Write-Info "Using specified version: $RequestedVersion" + return $RequestedVersion + } + + Write-Info "Fetching latest release information" + + $releaseUrl = "https://api.github.com/repos/${Repository}/releases/latest" + $release = Invoke-RestMethod -Uri $releaseUrl -TimeoutSec 20 + $latestVersion = $release.tag_name + + if ([string]::IsNullOrWhiteSpace($latestVersion)) { + Write-ErrorMessage "Could not determine latest version" + exit 1 + } + + Write-Info "Latest version: $latestVersion" + return $latestVersion +} + +function Get-DownloadInfo { + param( + [string]$ResolvedVersion, + [string]$OS, + [string]$Arch, + [string]$Repository, + [string]$BinaryName + ) + + $fileName = "${BinaryName}-${ResolvedVersion}-${OS}-${Arch}.exe" + $baseUrl = "https://github.com/${Repository}/releases/download/${ResolvedVersion}" + + return @{ + FileName = $fileName + BinaryUrl = "${baseUrl}/${fileName}" + ChecksumUrl = "${baseUrl}/SHA256SUMS" + } +} + +function Download-File { + param( + [string]$Url, + [string]$OutputPath + ) + + Write-Info "Downloading $Url" + $ProgressPreference = "SilentlyContinue" + Invoke-WebRequest -Uri $Url -OutFile $OutputPath -TimeoutSec 120 +} + +function Test-Checksum { + param( + [string]$BinaryPath, + [string]$ChecksumPath, + [string]$FileName + ) + + $checksumLine = Get-Content $ChecksumPath | Where-Object { + $parts = $_ -split "\s+" + $parts.Count -ge 2 -and $parts[1] -eq $FileName + } | Select-Object -First 1 + + if ([string]::IsNullOrWhiteSpace($checksumLine)) { + Write-ErrorMessage "No checksum found for $FileName in SHA256SUMS" + exit 1 + } + + $expected = ($checksumLine -split "\s+")[0].ToLowerInvariant() + $actual = (Get-FileHash -Algorithm SHA256 -Path $BinaryPath).Hash.ToLowerInvariant() + + if ($actual -ne $expected) { + Write-ErrorMessage "Checksum verification failed for $FileName" + Write-ErrorMessage "Expected: $expected" + Write-ErrorMessage "Actual: $actual" + exit 1 + } + + Write-Info "Checksum verified" +} + +function Install-CLI { + param( + [string]$TempFile, + [string]$TargetDir, + [string]$BinaryName + ) + + if (-not (Test-Path $TargetDir)) { + Write-Info "Creating directory: $TargetDir" + New-Item -ItemType Directory -Path $TargetDir -Force | Out-Null + } + + $targetFile = Join-Path $TargetDir "${BinaryName}.exe" + Write-Info "Installing CLI to $targetFile" + + Stop-Process -Name $BinaryName -Force -ErrorAction SilentlyContinue + Copy-Item -Path $TempFile -Destination $targetFile -Force + + $userPath = [Environment]::GetEnvironmentVariable("Path", "User") + $pathParts = @() + if (-not [string]::IsNullOrWhiteSpace($userPath)) { + $pathParts = $userPath -split ";" + } + + if ($pathParts -notcontains $TargetDir) { + Write-Info "Adding $TargetDir to user PATH" + $newPath = if ([string]::IsNullOrWhiteSpace($userPath)) { $TargetDir } else { "$userPath;$TargetDir" } + [Environment]::SetEnvironmentVariable("Path", $newPath, "User") + Write-Warn "Restart your terminal for PATH changes to take effect" + } + + Write-Info "CLI installed successfully at $targetFile" + return $targetFile +} + +function Test-Installation { + param([string]$CliPath) + + if (-not (Test-Path $CliPath)) { + Write-Warn "CLI not found at expected location: $CliPath" + return + } + + try { + & $CliPath --version + if ($LASTEXITCODE -eq 0) { + Write-Info "Installation verified successfully" + return + } + } + catch { + Write-Warn "Could not execute version check: $_" + } + + Write-Warn "Could not verify CLI execution, but the binary was installed" +} + +function Main { + $platform = Get-Platform + $resolvedVersion = Get-ReleaseVersion -RequestedVersion $Version -Repository $GitHubRepo + $downloadInfo = Get-DownloadInfo -ResolvedVersion $resolvedVersion -OS $platform.OS -Arch $platform.Arch -Repository $GitHubRepo -BinaryName $CliName + + Write-Info "Download URL: $($downloadInfo.BinaryUrl)" + + $tempBinary = [System.IO.Path]::GetTempFileName() + $tempSums = [System.IO.Path]::GetTempFileName() + + try { + Download-File -Url $downloadInfo.BinaryUrl -OutputPath $tempBinary + Download-File -Url $downloadInfo.ChecksumUrl -OutputPath $tempSums + Test-Checksum -BinaryPath $tempBinary -ChecksumPath $tempSums -FileName $downloadInfo.FileName + + $cliPath = Install-CLI -TempFile $tempBinary -TargetDir $InstallDir -BinaryName $CliName + Test-Installation -CliPath $cliPath + Write-Info "Installation complete" + } + finally { + Remove-Item $tempBinary -Force -ErrorAction SilentlyContinue + Remove-Item $tempSums -Force -ErrorAction SilentlyContinue + } +} + +Main diff --git a/tools/scripts/install.sh b/tools/scripts/install.sh new file mode 100755 index 0000000000..634c5987bb --- /dev/null +++ b/tools/scripts/install.sh @@ -0,0 +1,272 @@ +#!/bin/sh +# +# Copyright 2026 The InfiniFlow Authors. All Rights Reserved. +# +# 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. +# + +set -eu + +INSTALL_DIR="${INSTALL_DIR:-/usr/local/bin}" +GITHUB_REPO="${GITHUB_REPO:-infiniflow/ragflow}" +CLI_NAME="${CLI_NAME:-ragflow_cli}" +VERSION="${VERSION:-latest}" + +RELEASE_API="https://api.github.com/repos/${GITHUB_REPO}/releases/latest" +RELEASE_BASE_URL="" +OS="" +ARCH="" +FILENAME="" +DOWNLOAD_URL="" +CHECKSUM_URL="" + +info() { + printf '[INFO] %s\n' "$1" >&2 +} + +warn() { + printf '[WARN] %s\n' "$1" >&2 +} + +error() { + printf '[ERROR] %s\n' "$1" >&2 +} + +need_cmd() { + if ! command -v "$1" >/dev/null 2>&1; then + error "$1 is required but not installed" + exit 1 + fi +} + +run_as_root() { + if "$@" 2>/dev/null; then + return 0 + fi + + if command -v sudo >/dev/null 2>&1; then + info "Requesting sudo permission for: $*" + sudo "$@" + return $? + fi + + error "Permission denied and sudo is not available: $*" + exit 1 +} + +detect_platform() { + os_name="$(uname -s)" + arch_name="$(uname -m)" + + case "$os_name" in + Linux) + OS="linux" + ;; + Darwin) + OS="darwin" + ;; + *) + error "Unsupported OS for install.sh: $os_name" + error "Use install.ps1 on Windows PowerShell" + exit 1 + ;; + esac + + case "$arch_name" in + x86_64|amd64) + ARCH="amd64" + ;; + aarch64|arm64) + ARCH="arm64" + ;; + *) + error "Unsupported architecture: $arch_name" + exit 1 + ;; + esac + + info "Detected platform: ${OS}/${ARCH}" +} + +resolve_version() { + if [ "$VERSION" != "latest" ]; then + info "Using specified version: $VERSION" + return + fi + + info "Fetching latest release information" + response="$(curl -sSfL "$RELEASE_API")" + VERSION="$(printf '%s\n' "$response" | sed -n 's/.*"tag_name":[[:space:]]*"\([^"]*\)".*/\1/p' | head -n 1)" + + if [ -z "$VERSION" ]; then + error "Could not determine latest version from GitHub" + exit 1 + fi + + info "Latest version: $VERSION" +} + +build_urls() { + FILENAME="${CLI_NAME}-${VERSION}-${OS}-${ARCH}" + RELEASE_BASE_URL="https://github.com/${GITHUB_REPO}/releases/download/${VERSION}" + DOWNLOAD_URL="${RELEASE_BASE_URL}/${FILENAME}" + CHECKSUM_URL="${RELEASE_BASE_URL}/SHA256SUMS" + + info "Download URL: $DOWNLOAD_URL" +} + +download_file() { + url="$1" + output="$2" + + if ! curl -sSfL "$url" -o "$output"; then + rm -f "$output" + error "Failed to download $url" + exit 1 + fi +} + +file_size() { + stat -f%z "$1" 2>/dev/null || stat -c%s "$1" 2>/dev/null || printf '0' +} + +sha256_file() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | awk '{print $1}' + return + fi + + if command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$1" | awk '{print $1}' + return + fi + + error "sha256sum or shasum is required to verify the download" + exit 1 +} + +verify_checksum() { + binary_file="$1" + sums_file="$2" + + expected="$(awk -v f="$FILENAME" '$2 == f {print $1}' "$sums_file" | head -n 1)" + if [ -z "$expected" ]; then + error "No checksum found for $FILENAME in SHA256SUMS" + exit 1 + fi + + actual="$(sha256_file "$binary_file")" + if [ "$actual" != "$expected" ]; then + error "Checksum verification failed for $FILENAME" + error "Expected: $expected" + error "Actual: $actual" + exit 1 + fi + + info "Checksum verified" +} + +download_cli() { + binary_file="$(mktemp)" + sums_file="$(mktemp)" + + info "Downloading CLI" + download_file "$DOWNLOAD_URL" "$binary_file" + + size="$(file_size "$binary_file")" + if [ "$size" -lt 100000 ]; then + warn "Downloaded file is unusually small (${size} bytes)" + fi + + info "Downloading SHA256SUMS" + download_file "$CHECKSUM_URL" "$sums_file" + verify_checksum "$binary_file" "$sums_file" + rm -f "$sums_file" + + printf '%s\n' "$binary_file" +} + +install_cli() { + source_file="$1" + target_file="${INSTALL_DIR}/${CLI_NAME}" + + info "Installing CLI to $target_file" + + if [ ! -d "$INSTALL_DIR" ]; then + run_as_root mkdir -p "$INSTALL_DIR" + fi + + if [ -w "$INSTALL_DIR" ]; then + mv "$source_file" "$target_file" + chmod 755 "$target_file" + else + run_as_root mv "$source_file" "$target_file" + run_as_root chmod 755 "$target_file" + fi + + info "CLI installed successfully at $target_file" +} + +verify_installation() { + cli_path="${INSTALL_DIR}/${CLI_NAME}" + + if [ ! -x "$cli_path" ]; then + warn "CLI may not be executable: $cli_path" + return + fi + + if "$cli_path" --version >/dev/null 2>&1; then + "$cli_path" --version + info "Installation verified successfully" + return + fi + + if "$cli_path" -h >/dev/null 2>&1 || "$cli_path" --help >/dev/null 2>&1; then + info "Installation verified successfully" + return + fi + + warn "Could not verify CLI execution, but the binary was installed" +} + +print_path_notice() { + case ":$PATH:" in + *":$INSTALL_DIR:"*) + ;; + *) + warn "$INSTALL_DIR is not in PATH" + warn "Add it with: export PATH=\"$INSTALL_DIR:\$PATH\"" + ;; + esac +} + +main() { + need_cmd curl + need_cmd uname + need_cmd mktemp + need_cmd awk + need_cmd sed + + detect_platform + resolve_version + build_urls + + temp_file="$(download_cli)" + install_cli "$temp_file" + verify_installation + print_path_notice + + info "Installation complete" +} + +main "$@"