Skip to content

Merge pull request #173 from dell/release/v5.14.0 #40

Merge pull request #173 from dell/release/v5.14.0

Merge pull request #173 from dell/release/v5.14.0 #40

---
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/**