Merge pull request #173 from dell/release/v5.14.0 #40
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| --- | |
| name: Release Artifacts | |
| 'on': | |
| push: | |
| tags: | |
| - 'v*' | |
| workflow_dispatch: | |
| inputs: | |
| version: | |
| description: 'Override VERSION when testing manually (defaults to VERSION file or tag).' | |
| required: false | |
| allow_snapshot: | |
| description: 'Allow VERSION values ending in -SNAPSHOT (for dry runs only).' | |
| required: false | |
| type: boolean | |
| concurrency: | |
| group: release-artifacts-${{ github.ref }} | |
| cancel-in-progress: true | |
| env: | |
| REGISTRY: ghcr.io | |
| IMAGE_NAME: dell/storage-performance-tool | |
| jobs: | |
| verify-ci: | |
| name: Verify CI Status | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Check CI workflow status | |
| if: github.event_name == 'push' | |
| uses: actions/github-script@v9 | |
| with: | |
| script: | | |
| const sha = context.sha; | |
| const ciCheckNames = ['Engine Build & Tests', 'Go Quality Suite']; | |
| const maxAttempts = 60; | |
| const intervalMs = 30000; | |
| console.log(`Checking CI status for commit ${sha}`); | |
| console.log(`Will poll up to ${maxAttempts} times (${maxAttempts * intervalMs / 1000}s) for: ${ciCheckNames.join(', ')}`); | |
| for (let attempt = 1; attempt <= maxAttempts; attempt++) { | |
| const { data: checkRuns } = await github.rest.checks.listForRef({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| ref: sha, | |
| }); | |
| const passed = new Set(); | |
| const pending = []; | |
| for (const name of ciCheckNames) { | |
| const run = checkRuns.check_runs.find(r => r.name === name); | |
| if (!run) { | |
| pending.push(name); | |
| console.log(`Attempt ${attempt}/${maxAttempts}: "${name}" not found yet.`); | |
| } else if (run.conclusion === 'success') { | |
| passed.add(name); | |
| } else if (run.conclusion) { | |
| core.setFailed(`CI check "${name}" failed (conclusion: ${run.conclusion}). Cannot publish release.`); | |
| return; | |
| } else { | |
| pending.push(name); | |
| console.log(`Attempt ${attempt}/${maxAttempts}: "${name}" still in progress...`); | |
| } | |
| } | |
| if (passed.size === ciCheckNames.length) { | |
| console.log(`All CI checks passed: ${[...passed].join(', ')}`); | |
| return; | |
| } | |
| console.log(`Attempt ${attempt}/${maxAttempts}: ${passed.size}/${ciCheckNames.length} passed, waiting on: ${pending.join(', ')}`); | |
| if (attempt < maxAttempts) { | |
| await new Promise(r => setTimeout(r, intervalMs)); | |
| } | |
| } | |
| core.setFailed(`CI checks did not complete within ${maxAttempts * intervalMs / 1000}s. Re-run this workflow after CI passes.`); | |
| prepare-version: | |
| name: Prepare Version | |
| needs: verify-ci | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.version.outputs.version }} | |
| tag: ${{ steps.version.outputs.tag }} | |
| artifact-prefix: ${{ steps.version.outputs.prefix }} | |
| allow-snapshot: ${{ steps.snapshot.outputs.allow }} | |
| major: ${{ steps.version.outputs.major }} | |
| minor: ${{ steps.version.outputs.minor }} | |
| is_prerelease: ${{ steps.version.outputs.is_prerelease }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 | |
| - name: Determine version/tag | |
| id: version | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| ref_type="${GITHUB_REF_TYPE:-}" | |
| ref_name="${GITHUB_REF_NAME:-}" | |
| version_input="${{ inputs.version }}" | |
| version_file="$(< VERSION)" | |
| version_file="${version_file//$'\n'/}" | |
| version="${version_file}" | |
| if [[ -n "${version_input}" ]]; then | |
| version="${version_input}" | |
| elif [[ "${ref_type}" == "tag" && -n "${ref_name}" ]]; then | |
| # Accept both vX.Y.Z and bare X.Y.Z tags | |
| if [[ "${ref_name}" == v* ]]; then | |
| version="${ref_name#v}" | |
| else | |
| version="${ref_name}" | |
| fi | |
| fi | |
| tag_name="${ref_name}" | |
| if [[ -z "${tag_name}" && -n "${version}" ]]; then | |
| tag_name="v${version}" | |
| fi | |
| echo "Detected version: ${version}" | |
| echo "Detected tag: ${tag_name}" | |
| if [[ -z "${version}" ]]; then | |
| echo "error: unable to resolve version" >&2 | |
| exit 1 | |
| fi | |
| base_version="${version%%-*}" | |
| IFS='.' read -r major minor patch <<< "${base_version}" | |
| is_prerelease="false" | |
| if [[ "${version}" == *-* ]]; then | |
| is_prerelease="true" | |
| fi | |
| { | |
| echo "version=${version}" | |
| echo "tag=${tag_name}" | |
| echo "prefix=spt-${version}" | |
| echo "major=${major}" | |
| echo "minor=${minor}" | |
| echo "is_prerelease=${is_prerelease}" | |
| } >> "${GITHUB_OUTPUT}" | |
| - name: Snapshot allowance | |
| id: snapshot | |
| shell: bash | |
| run: | | |
| allow="${{ inputs.allow_snapshot && 'true' || 'false' }}" | |
| echo "allow=${allow}" >> "${GITHUB_OUTPUT}" | |
| - name: Verify version sync | |
| shell: bash | |
| run: | | |
| args=(--expected "${{ steps.version.outputs.version }}") | |
| if [[ "${{ steps.snapshot.outputs.allow }}" == 'true' ]]; then | |
| args+=(--allow-snapshot) | |
| fi | |
| scripts/release/check_version_sync.sh "${args[@]}" | |
| cli-binaries: | |
| name: Build CLI Binaries | |
| needs: prepare-version | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| env: | |
| CGO_ENABLED: "0" | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup Go | |
| uses: actions/setup-go@v7 | |
| with: | |
| go-version-file: cli/go.mod | |
| cache: true | |
| cache-dependency-path: cli/go.sum | |
| - name: Build multi-platform binaries | |
| run: make -C cli build-all VERSION=${{ needs.prepare-version.outputs.version }} | |
| - name: Verify Linux ARM64 binary | |
| run: test -s cli/build/spt-linux-arm64 | |
| - name: Verify Linux binaries are static | |
| run: | | |
| set -euo pipefail | |
| file cli/build/spt-linux-amd64 | tee /tmp/spt-linux-amd64.file | |
| file cli/build/spt-linux-arm64 | tee /tmp/spt-linux-arm64.file | |
| grep -q 'statically linked' /tmp/spt-linux-amd64.file | |
| grep -q 'statically linked' /tmp/spt-linux-arm64.file | |
| ldd cli/build/spt-linux-amd64 > /tmp/spt-linux-amd64.ldd 2>&1 || true | |
| grep -q 'not a dynamic executable' /tmp/spt-linux-amd64.ldd | |
| - name: Package binaries | |
| run: | | |
| set -euo pipefail | |
| version="${{ needs.prepare-version.outputs.version }}" | |
| release_root="release" | |
| if [[ -n "${ACT:-}" && -d /release ]]; then | |
| release_root="/release" | |
| fi | |
| cli_dir="${release_root}/cli" | |
| mkdir -p "${cli_dir}" | |
| rm -f \ | |
| "${cli_dir}/spt-${version}-linux-amd64" \ | |
| "${cli_dir}/spt-${version}-linux-amd64.gz" \ | |
| "${cli_dir}/spt-${version}-linux-arm64" \ | |
| "${cli_dir}/spt-${version}-linux-arm64.gz" \ | |
| "${cli_dir}/spt-${version}-darwin-amd64" \ | |
| "${cli_dir}/spt-${version}-darwin-amd64.gz" \ | |
| "${cli_dir}/spt-${version}-darwin-arm64" \ | |
| "${cli_dir}/spt-${version}-darwin-arm64.gz" \ | |
| "${cli_dir}/spt-${version}-windows-amd64.exe" \ | |
| "${cli_dir}/spt-${version}-windows-amd64.zip" \ | |
| "${cli_dir}/SHA256SUMS" | |
| cp cli/build/spt-linux-amd64 "${cli_dir}/spt-${version}-linux-amd64" | |
| gzip -n "${cli_dir}/spt-${version}-linux-amd64" | |
| cp cli/build/spt-linux-arm64 "${cli_dir}/spt-${version}-linux-arm64" | |
| gzip -n "${cli_dir}/spt-${version}-linux-arm64" | |
| cp cli/build/spt-darwin-amd64 "${cli_dir}/spt-${version}-darwin-amd64" | |
| gzip -n "${cli_dir}/spt-${version}-darwin-amd64" | |
| cp cli/build/spt-darwin-arm64 "${cli_dir}/spt-${version}-darwin-arm64" | |
| gzip -n "${cli_dir}/spt-${version}-darwin-arm64" | |
| cp cli/build/spt-windows-amd64.exe "${cli_dir}/spt-${version}-windows-amd64.exe" | |
| python3 - "${cli_dir}" "spt-${version}-windows-amd64.exe" "spt-${version}-windows-amd64.zip" <<'PY' | |
| import os | |
| import sys | |
| import zipfile | |
| cli_dir, exe_name, zip_name = sys.argv[1:] | |
| with zipfile.ZipFile(os.path.join(cli_dir, zip_name), "w", zipfile.ZIP_DEFLATED) as zf: | |
| zf.write(os.path.join(cli_dir, exe_name), arcname=exe_name) | |
| PY | |
| rm "${cli_dir}/spt-${version}-windows-amd64.exe" | |
| cat <<'EOF' > "${cli_dir}/README.txt" | |
| Dell Storage Performance Tool (SPT) CLI binaries | |
| https://github.com/dell/storage-performance-tool | |
| File naming: spt-<version>-<os>-<arch>.<ext> | |
| - Linux (amd64, arm64): gzip binaries | |
| - macOS (amd64, arm64): gzip binaries | |
| - Windows (amd64): zip archive with .exe | |
| EOF | |
| expected_assets=( | |
| "spt-${version}-linux-amd64.gz" | |
| "spt-${version}-linux-arm64.gz" | |
| "spt-${version}-darwin-amd64.gz" | |
| "spt-${version}-darwin-arm64.gz" | |
| "spt-${version}-windows-amd64.zip" | |
| ) | |
| for asset in "${expected_assets[@]}"; do | |
| if [[ ! -s "${cli_dir}/${asset}" ]]; then | |
| echo "Expected CLI asset ${asset} is missing or empty" >&2 | |
| exit 1 | |
| fi | |
| done | |
| (cd "${cli_dir}" && sha256sum "${expected_assets[@]}" > SHA256SUMS) | |
| for asset in "${expected_assets[@]}"; do | |
| count=$(awk -v file="${asset}" '{ name=$2; sub(/^\*/, "", name); if (name == file) count++ } END { print count + 0 }' "${cli_dir}/SHA256SUMS") | |
| if [[ "${count}" != "1" ]]; then | |
| echo "Expected exactly one SHA256SUMS entry for ${asset}; found ${count}" >&2 | |
| cat "${cli_dir}/SHA256SUMS" >&2 | |
| exit 1 | |
| fi | |
| done | |
| unexpected=$(awk '{ name=$2; sub(/^\*/, "", name); if (name !~ /\.(gz|zip)$/) print name }' "${cli_dir}/SHA256SUMS") | |
| if [[ -n "${unexpected}" ]]; then | |
| echo "SHA256SUMS contains unexpected non-archive entries:" >&2 | |
| echo "${unexpected}" >&2 | |
| exit 1 | |
| fi | |
| - name: Upload CLI artifacts | |
| if: ${{ !env.ACT }} | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: cli-${{ needs.prepare-version.outputs.version }} | |
| path: release/cli/** | |
| if-no-files-found: error | |
| engine-bundle: | |
| name: Build Engine Bundle | |
| needs: prepare-version | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 | |
| - name: Install JDK 21 for act | |
| if: ${{ env.ACT }} | |
| run: | | |
| sudo apt-get update | |
| sudo DEBIAN_FRONTEND=noninteractive apt-get install \ | |
| -y \ | |
| --no-install-recommends \ | |
| openjdk-21-jdk \ | |
| ca-certificates | |
| java -version | |
| - name: Setup Java 21 (Temurin) | |
| if: ${{ !env.ACT }} | |
| uses: actions/setup-java@v5.6.0 | |
| with: | |
| distribution: 'temurin' | |
| java-version: '21' | |
| - name: Install RDMA build dependencies | |
| run: | | |
| sudo apt-get update -qq | |
| sudo apt-get install -y --no-install-recommends \ | |
| cmake \ | |
| libibverbs-dev \ | |
| librdmacm-dev | |
| - name: Setup Gradle | |
| uses: gradle/actions/setup-gradle@v6 | |
| - name: Build bundle distribution | |
| working-directory: engine | |
| run: ./gradlew :bundle:distZip --no-daemon --stacktrace | |
| - name: Stage bundle artifact | |
| run: | | |
| set -euo pipefail | |
| version="${{ needs.prepare-version.outputs.version }}" | |
| release_root="release" | |
| if [[ -n "${ACT:-}" && -d /release ]]; then | |
| release_root="/release" | |
| fi | |
| engine_dir="${release_root}/engine" | |
| mkdir -p "${engine_dir}" | |
| dist_zip="engine/bundle/build/distributions/spt-bundle-${version}.zip" | |
| if [[ ! -f "${dist_zip}" ]]; then | |
| echo "Expected bundle ${dist_zip} not found" >&2 | |
| ls engine/bundle/build/distributions >&2 || true | |
| exit 1 | |
| fi | |
| cp "${dist_zip}" "${engine_dir}/spt-bundle-${version}.zip" | |
| - name: Upload bundle artifact | |
| if: ${{ !env.ACT }} | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: engine-bundle-${{ needs.prepare-version.outputs.version }} | |
| path: release/engine/** | |
| if-no-files-found: error | |
| docker-image: | |
| name: Build and Push Docker Image | |
| needs: prepare-version | |
| if: ${{ startsWith(github.ref, 'refs/tags/') }} | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| packages: write | |
| id-token: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v7 | |
| - name: Set up QEMU | |
| uses: docker/setup-qemu-action@v4 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v4 | |
| - name: Log in to GHCR | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Extract Docker metadata | |
| id: meta | |
| uses: docker/metadata-action@v6 | |
| with: | |
| images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} | |
| tags: | | |
| # Exact version tag: v5.0.2 | |
| type=raw,value=${{ needs.prepare-version.outputs.tag }} | |
| # Floating minor tag: v5.0 (only for stable releases) | |
| type=semver,pattern=v{{major}}.{{minor}},value=${{ needs.prepare-version.outputs.version }},enable=${{ needs.prepare-version.outputs.is_prerelease == 'false' }} | |
| # Floating major tag: v5 (only for stable releases) | |
| type=semver,pattern=v{{major}},value=${{ needs.prepare-version.outputs.version }},enable=${{ needs.prepare-version.outputs.is_prerelease == 'false' }} | |
| # Latest tag (only for stable releases) | |
| type=raw,value=latest,enable=${{ needs.prepare-version.outputs.is_prerelease == 'false' }} | |
| labels: | | |
| org.opencontainers.image.title=Dell Storage Performance Tool | |
| org.opencontainers.image.description=High-performance S3-compatible storage benchmarking tool | |
| org.opencontainers.image.vendor=Dell Inc. | |
| - name: Install JDK 21 | |
| uses: actions/setup-java@v5.6.0 | |
| with: | |
| distribution: 'temurin' | |
| java-version: '21' | |
| - name: Setup Gradle | |
| uses: gradle/actions/setup-gradle@v6 | |
| - name: Install RDMA build dependencies | |
| run: | | |
| sudo apt-get update -qq | |
| sudo apt-get install -y --no-install-recommends \ | |
| libibverbs-dev \ | |
| librdmacm-dev | |
| - name: Build Engine distribution | |
| working-directory: engine | |
| run: ./gradlew :bundle:assembleDist --no-daemon | |
| - name: Verify version alignment | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| expected_version="${{ needs.prepare-version.outputs.version }}" | |
| version_output=$(java -jar engine/bundle/build/dist/spt.jar --version 2>&1 || true) | |
| echo "Raw version output: ${version_output}" | |
| jar_version=$(echo "${version_output}" | grep -oE 'v?[0-9]+\.[0-9]+\.[0-9]+[^[:space:]]*' | sed 's/^v//' | head -1 || echo "unknown") | |
| echo "Expected version: ${expected_version}" | |
| echo "JAR version: ${jar_version}" | |
| if [[ "${jar_version}" != "${expected_version}" ]]; then | |
| echo "::error::Version mismatch! Tag/input version (${expected_version}) does not match JAR version (${jar_version})" | |
| echo "Ensure VERSION file and Gradle version are updated before tagging." | |
| exit 1 | |
| fi | |
| echo "Version alignment verified: ${expected_version}" | |
| - name: Build and push Docker image | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: engine/bundle | |
| file: engine/bundle/Dockerfile | |
| platforms: linux/amd64,linux/arm64 | |
| push: true | |
| tags: ${{ steps.meta.outputs.tags }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| build-args: | | |
| SPT_VERSION=${{ needs.prepare-version.outputs.version }} | |
| SPT_RELEASE_VERSION=${{ needs.prepare-version.outputs.version }} | |
| provenance: true | |
| sbom: true | |
| cache-from: type=gha,scope=spt-engine | |
| cache-to: type=gha,mode=max,scope=spt-engine | |
| - name: Verify pushed Docker manifest | |
| env: | |
| IMAGE_REF: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.prepare-version.outputs.tag }} | |
| run: | | |
| set -euo pipefail | |
| docker buildx imagetools inspect "${IMAGE_REF}" --raw > manifest.json | |
| python3 - <<'PY' | |
| import json | |
| import os | |
| import sys | |
| with open("manifest.json", encoding="utf-8") as fh: | |
| manifest = json.load(fh) | |
| platforms = set() | |
| for entry in manifest.get("manifests", []): | |
| platform = entry.get("platform") or {} | |
| os_name = platform.get("os") | |
| arch = platform.get("architecture") | |
| if os_name and arch: | |
| platforms.add(f"{os_name}/{arch}") | |
| required = {"linux/amd64", "linux/arm64"} | |
| missing = sorted(required - platforms) | |
| print(f"Manifest {os.environ['IMAGE_REF']} platforms: {sorted(platforms)}") | |
| if missing: | |
| print(f"Missing required platforms: {missing}", file=sys.stderr) | |
| sys.exit(1) | |
| PY | |
| - name: Output image info | |
| run: | | |
| echo "## Docker Image Published" >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo "**Version:** ${{ needs.prepare-version.outputs.version }}" >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo "**Tags:**" >> "$GITHUB_STEP_SUMMARY" | |
| echo '```' >> "$GITHUB_STEP_SUMMARY" | |
| echo "${{ steps.meta.outputs.tags }}" >> "$GITHUB_STEP_SUMMARY" | |
| echo '```' >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo "**Platforms:** linux/amd64, linux/arm64" >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo "**Supply Chain:**" >> "$GITHUB_STEP_SUMMARY" | |
| echo "- SBOM attestation generated" >> "$GITHUB_STEP_SUMMARY" | |
| echo "- Provenance attestation generated" >> "$GITHUB_STEP_SUMMARY" | |
| publish-release: | |
| name: Publish GitHub Release | |
| needs: | |
| - prepare-version | |
| - cli-binaries | |
| - engine-bundle | |
| - docker-image | |
| if: ${{ startsWith(github.ref, 'refs/tags/') }} | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Skip publish when running under act | |
| if: ${{ env.ACT }} | |
| run: echo "Skipping GitHub Release publish when running under act." | |
| - name: Download CLI artifacts | |
| if: ${{ !env.ACT }} | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: cli-${{ needs.prepare-version.outputs.version }} | |
| path: release/cli | |
| - name: Download Engine bundle | |
| if: ${{ !env.ACT }} | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: engine-bundle-${{ needs.prepare-version.outputs.version }} | |
| path: release/engine | |
| - name: Publish release | |
| if: ${{ !env.ACT }} | |
| uses: softprops/action-gh-release@v3 | |
| with: | |
| tag_name: ${{ needs.prepare-version.outputs.tag }} | |
| prerelease: ${{ contains(needs.prepare-version.outputs.version, '-') }} | |
| files: | | |
| release/cli/** | |
| release/engine/** |