Add automated release workflow with validation
This commit introduces a comprehensive GitHub Actions workflow for creating releases, ensuring all artifacts are validated before upload. Changes: - Add .github/workflows/release.yml: Manual workflow_dispatch trigger that builds, validates, and creates draft releases - Update scripts/validate-release.sh: Add --skip-docker flag to allow validation without Docker image checks Key features: - Validation runs BEFORE any assets are uploaded - If validation fails, no release is created - checksums.txt and artifacts come from the same build - No manual steps between validation and upload - Checksums uploaded first, then all other assets - Creates draft release for manual review before publishing The workflow ensures that checksums.txt cannot drift from binaries by running the entire build-validate-upload pipeline atomically.
This commit is contained in:
parent
4c4fd3a99b
commit
e12980e351
2 changed files with 213 additions and 33 deletions
150
.github/workflows/release.yml
vendored
Normal file
150
.github/workflows/release.yml
vendored
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: 'Version number (e.g., 4.28.1)'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
create-release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: '1.24'
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
# Install zip for Windows binaries
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y zip
|
||||||
|
|
||||||
|
# Install Helm for chart packaging
|
||||||
|
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
|
||||||
|
|
||||||
|
- name: Build release artifacts
|
||||||
|
run: |
|
||||||
|
echo "Building release v${{ inputs.version }}..."
|
||||||
|
./scripts/build-release.sh ${{ inputs.version }}
|
||||||
|
|
||||||
|
- name: Validate release artifacts
|
||||||
|
run: |
|
||||||
|
echo "Validating release v${{ inputs.version }}..."
|
||||||
|
# Note: Docker image validation is skipped in this workflow
|
||||||
|
# since we're only validating the built artifacts (tarballs, binaries, checksums)
|
||||||
|
# Docker images should be validated separately before release
|
||||||
|
|
||||||
|
# Run validation with --skip-docker flag
|
||||||
|
./scripts/validate-release.sh ${{ inputs.version }} --skip-docker || {
|
||||||
|
echo "❌ Release validation failed!"
|
||||||
|
echo "The workflow will now fail. No release will be created."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Create draft release
|
||||||
|
id: create_release
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
VERSION="${{ inputs.version }}"
|
||||||
|
TAG="v${VERSION}"
|
||||||
|
|
||||||
|
echo "Creating draft release for ${TAG}..."
|
||||||
|
|
||||||
|
# Create draft release with placeholder body
|
||||||
|
gh release create "${TAG}" \
|
||||||
|
--draft \
|
||||||
|
--title "Pulse ${TAG}" \
|
||||||
|
--notes "Release ${TAG}
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
|
||||||
|
<!-- TODO: Add release notes before publishing -->
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Quick install on Linux:
|
||||||
|
\`\`\`bash
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/rcourtman/Pulse/main/install.sh | sudo bash
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
Or download platform-specific archives below."
|
||||||
|
|
||||||
|
echo "release_url=$(gh release view ${TAG} --json url -q .url)" >> $GITHUB_OUTPUT
|
||||||
|
echo "tag=${TAG}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Upload checksums.txt first
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
TAG="${{ steps.create_release.outputs.tag }}"
|
||||||
|
|
||||||
|
echo "Uploading checksums.txt..."
|
||||||
|
gh release upload "${TAG}" release/checksums.txt
|
||||||
|
|
||||||
|
# Also upload individual .sha256 files
|
||||||
|
echo "Uploading individual checksum files..."
|
||||||
|
gh release upload "${TAG}" release/*.sha256
|
||||||
|
|
||||||
|
- name: Upload release assets
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
TAG="${{ steps.create_release.outputs.tag }}"
|
||||||
|
|
||||||
|
echo "Uploading release assets..."
|
||||||
|
|
||||||
|
# Upload tarballs
|
||||||
|
gh release upload "${TAG}" release/*.tar.gz
|
||||||
|
|
||||||
|
# Upload Windows zip files
|
||||||
|
gh release upload "${TAG}" release/*.zip
|
||||||
|
|
||||||
|
# Upload Helm chart if it exists
|
||||||
|
if ls release/*.tgz 1> /dev/null 2>&1; then
|
||||||
|
echo "Uploading Helm chart..."
|
||||||
|
gh release upload "${TAG}" release/*.tgz
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Upload install.sh
|
||||||
|
gh release upload "${TAG}" release/install.sh
|
||||||
|
|
||||||
|
# Upload standalone binaries (for direct download by installers)
|
||||||
|
echo "Uploading standalone binaries..."
|
||||||
|
gh release upload "${TAG}" release/pulse-sensor-proxy-*
|
||||||
|
gh release upload "${TAG}" release/pulse-host-agent-*
|
||||||
|
|
||||||
|
- name: Output release information
|
||||||
|
run: |
|
||||||
|
echo "✅ Release draft created successfully!"
|
||||||
|
echo ""
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo "📦 Release: ${{ steps.create_release.outputs.tag }}"
|
||||||
|
echo "🔗 URL: ${{ steps.create_release.outputs.release_url }}"
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo ""
|
||||||
|
echo "⚠️ IMPORTANT: This release is in DRAFT status"
|
||||||
|
echo ""
|
||||||
|
echo "Next steps:"
|
||||||
|
echo "1. Review the release at the URL above"
|
||||||
|
echo "2. Update the release notes with changes since last release"
|
||||||
|
echo "3. Publish the release manually when ready"
|
||||||
|
echo ""
|
||||||
|
echo "All artifacts have been uploaded and validated."
|
||||||
|
echo "checksums.txt matches all uploaded binaries."
|
||||||
|
echo ""
|
||||||
|
|
@ -3,9 +3,10 @@
|
||||||
# Pulse Release Validation Script
|
# Pulse Release Validation Script
|
||||||
# Comprehensive artifact validation to prevent missing files/binaries in releases
|
# Comprehensive artifact validation to prevent missing files/binaries in releases
|
||||||
#
|
#
|
||||||
# Usage: ./scripts/validate-release.sh <pulse-version> [image] [release-dir]
|
# Usage: ./scripts/validate-release.sh <pulse-version> [image] [release-dir] [--skip-docker]
|
||||||
# Example: ./scripts/validate-release.sh 4.26.2
|
# Example: ./scripts/validate-release.sh 4.26.2
|
||||||
# ./scripts/validate-release.sh 4.26.2 rcourtman/pulse:v4.26.2 release
|
# ./scripts/validate-release.sh 4.26.2 rcourtman/pulse:v4.26.2 release
|
||||||
|
# ./scripts/validate-release.sh 4.26.2 --skip-docker
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
|
@ -33,17 +34,41 @@ warn() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if [ $# -lt 1 ]; then
|
if [ $# -lt 1 ]; then
|
||||||
error "Usage: $0 <pulse-version> [image] [release-dir]"
|
error "Usage: $0 <pulse-version> [image] [release-dir] [--skip-docker]"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
PULSE_VERSION=$1
|
PULSE_VERSION=$1
|
||||||
PULSE_TAG="v${PULSE_VERSION}"
|
PULSE_TAG="v${PULSE_VERSION}"
|
||||||
IMAGE=${2:-"rcourtman/pulse:${PULSE_TAG}"}
|
SKIP_DOCKER=false
|
||||||
RELEASE_DIR=${3:-"release"}
|
|
||||||
|
# Parse arguments
|
||||||
|
shift
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--skip-docker)
|
||||||
|
SKIP_DOCKER=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
if [ -z "${IMAGE:-}" ]; then
|
||||||
|
IMAGE="$1"
|
||||||
|
elif [ -z "${RELEASE_DIR:-}" ]; then
|
||||||
|
RELEASE_DIR="$1"
|
||||||
|
fi
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Set defaults
|
||||||
|
IMAGE=${IMAGE:-"rcourtman/pulse:${PULSE_TAG}"}
|
||||||
|
RELEASE_DIR=${RELEASE_DIR:-"release"}
|
||||||
|
|
||||||
# Validate prerequisites
|
# Validate prerequisites
|
||||||
command -v docker >/dev/null || { error "docker is required"; exit 1; }
|
if [ "$SKIP_DOCKER" = false ]; then
|
||||||
|
command -v docker >/dev/null || { error "docker is required (use --skip-docker to skip Docker validation)"; exit 1; }
|
||||||
|
fi
|
||||||
[ -d "$RELEASE_DIR" ] || { error "release dir not found: $RELEASE_DIR"; exit 1; }
|
[ -d "$RELEASE_DIR" ] || { error "release dir not found: $RELEASE_DIR"; exit 1; }
|
||||||
|
|
||||||
# Create temp directory for extractions
|
# Create temp directory for extractions
|
||||||
|
|
@ -58,43 +83,48 @@ echo ""
|
||||||
#=============================================================================
|
#=============================================================================
|
||||||
# DOCKER IMAGE VALIDATION
|
# DOCKER IMAGE VALIDATION
|
||||||
#=============================================================================
|
#=============================================================================
|
||||||
info "=== Docker Image Validation ==="
|
if [ "$SKIP_DOCKER" = false ]; then
|
||||||
|
info "=== Docker Image Validation ==="
|
||||||
|
|
||||||
# Validate VERSION file in container
|
# Validate VERSION file in container
|
||||||
info "Checking VERSION file in Docker image..."
|
info "Checking VERSION file in Docker image..."
|
||||||
docker run --rm --entrypoint /bin/sh -e EXPECTED_VERSION="$PULSE_VERSION" "$IMAGE" -c 'set -euo pipefail; actual=$(cat /VERSION | tr -d "\r\n"); [ "$actual" = "$EXPECTED_VERSION" ] || { echo "VERSION mismatch: expected=$EXPECTED_VERSION actual=$actual" >&2; exit 1; }' || { error "VERSION file mismatch in Docker image"; exit 1; }
|
docker run --rm --entrypoint /bin/sh -e EXPECTED_VERSION="$PULSE_VERSION" "$IMAGE" -c 'set -euo pipefail; actual=$(cat /VERSION | tr -d "\r\n"); [ "$actual" = "$EXPECTED_VERSION" ] || { echo "VERSION mismatch: expected=$EXPECTED_VERSION actual=$actual" >&2; exit 1; }' || { error "VERSION file mismatch in Docker image"; exit 1; }
|
||||||
success "VERSION file correct: $PULSE_VERSION"
|
success "VERSION file correct: $PULSE_VERSION"
|
||||||
|
|
||||||
# Validate all required scripts exist and are executable
|
# Validate all required scripts exist and are executable
|
||||||
info "Checking installer/uninstaller scripts in /opt/pulse/scripts/..."
|
info "Checking installer/uninstaller scripts in /opt/pulse/scripts/..."
|
||||||
docker run --rm --entrypoint /bin/sh "$IMAGE" -c 'set -euo pipefail; cd /opt/pulse/scripts; required="install-docker-agent.sh install-container-agent.sh install-host-agent.sh install-host-agent.ps1 uninstall-host-agent.sh uninstall-host-agent.ps1 install-sensor-proxy.sh install-docker.sh"; for f in $required; do [ -f "$f" ] || { echo "missing script $f" >&2; exit 1; }; case "$f" in *.sh|*.ps1) [ -x "$f" ] || { echo "$f not executable" >&2; exit 1; };; esac; done; echo "All scripts present and executable"' || { error "Script validation failed"; exit 1; }
|
docker run --rm --entrypoint /bin/sh "$IMAGE" -c 'set -euo pipefail; cd /opt/pulse/scripts; required="install-docker-agent.sh install-container-agent.sh install-host-agent.sh install-host-agent.ps1 uninstall-host-agent.sh uninstall-host-agent.ps1 install-sensor-proxy.sh install-docker.sh"; for f in $required; do [ -f "$f" ] || { echo "missing script $f" >&2; exit 1; }; case "$f" in *.sh|*.ps1) [ -x "$f" ] || { echo "$f not executable" >&2; exit 1; };; esac; done; echo "All scripts present and executable"' || { error "Script validation failed"; exit 1; }
|
||||||
success "All installer/uninstaller scripts present and executable"
|
success "All installer/uninstaller scripts present and executable"
|
||||||
|
|
||||||
# Validate all required binaries exist and are non-empty
|
# Validate all required binaries exist and are non-empty
|
||||||
info "Checking downloadable binaries in /opt/pulse/bin/..."
|
info "Checking downloadable binaries in /opt/pulse/bin/..."
|
||||||
docker run --rm --entrypoint /bin/sh "$IMAGE" -c 'set -euo pipefail; cd /opt/pulse/bin; required="pulse pulse-docker-agent pulse-docker-agent-linux-amd64 pulse-docker-agent-linux-arm64 pulse-docker-agent-linux-armv7 pulse-docker-agent-linux-armv6 pulse-docker-agent-linux-386 pulse-host-agent-linux-amd64 pulse-host-agent-linux-arm64 pulse-host-agent-linux-armv7 pulse-host-agent-linux-armv6 pulse-host-agent-linux-386 pulse-host-agent-darwin-amd64 pulse-host-agent-darwin-arm64 pulse-host-agent-windows-amd64.exe pulse-host-agent-windows-amd64 pulse-host-agent-windows-arm64.exe pulse-host-agent-windows-arm64 pulse-sensor-proxy pulse-sensor-proxy-linux-amd64 pulse-sensor-proxy-linux-arm64 pulse-sensor-proxy-linux-armv7 pulse-sensor-proxy-linux-armv6 pulse-sensor-proxy-linux-386"; for f in $required; do [ -e "$f" ] || { echo "missing binary $f" >&2; exit 1; }; [ -s "$f" ] || { echo "empty binary $f" >&2; exit 1; }; done; [ "$(readlink pulse-host-agent-windows-amd64)" = "pulse-host-agent-windows-amd64.exe" ] || { echo "windows amd64 symlink broken" >&2; exit 1; }; [ "$(readlink pulse-host-agent-windows-arm64)" = "pulse-host-agent-windows-arm64.exe" ] || { echo "windows arm64 symlink broken" >&2; exit 1; }; echo "All binaries present"' || { error "Binary validation failed"; exit 1; }
|
docker run --rm --entrypoint /bin/sh "$IMAGE" -c 'set -euo pipefail; cd /opt/pulse/bin; required="pulse pulse-docker-agent pulse-docker-agent-linux-amd64 pulse-docker-agent-linux-arm64 pulse-docker-agent-linux-armv7 pulse-docker-agent-linux-armv6 pulse-docker-agent-linux-386 pulse-host-agent-linux-amd64 pulse-host-agent-linux-arm64 pulse-host-agent-linux-armv7 pulse-host-agent-linux-armv6 pulse-host-agent-linux-386 pulse-host-agent-darwin-amd64 pulse-host-agent-darwin-arm64 pulse-host-agent-windows-amd64.exe pulse-host-agent-windows-amd64 pulse-host-agent-windows-arm64.exe pulse-host-agent-windows-arm64 pulse-sensor-proxy pulse-sensor-proxy-linux-amd64 pulse-sensor-proxy-linux-arm64 pulse-sensor-proxy-linux-armv7 pulse-sensor-proxy-linux-armv6 pulse-sensor-proxy-linux-386"; for f in $required; do [ -e "$f" ] || { echo "missing binary $f" >&2; exit 1; }; [ -s "$f" ] || { echo "empty binary $f" >&2; exit 1; }; done; [ "$(readlink pulse-host-agent-windows-amd64)" = "pulse-host-agent-windows-amd64.exe" ] || { echo "windows amd64 symlink broken" >&2; exit 1; }; [ "$(readlink pulse-host-agent-windows-arm64)" = "pulse-host-agent-windows-arm64.exe" ] || { echo "windows arm64 symlink broken" >&2; exit 1; }; echo "All binaries present"' || { error "Binary validation failed"; exit 1; }
|
||||||
success "All downloadable binaries present (24 binaries + 2 Windows symlinks)"
|
success "All downloadable binaries present (24 binaries + 2 Windows symlinks)"
|
||||||
|
|
||||||
# Validate version embedding in Docker image binaries
|
# Validate version embedding in Docker image binaries
|
||||||
info "Validating version embedding in Docker image binaries..."
|
info "Validating version embedding in Docker image binaries..."
|
||||||
|
|
||||||
# Pulse server binary
|
# Pulse server binary
|
||||||
docker run --rm --entrypoint /app/pulse "$IMAGE" version 2>/dev/null | grep -Fx "Pulse $PULSE_TAG" >/dev/null || { error "Pulse server version mismatch"; exit 1; }
|
docker run --rm --entrypoint /app/pulse "$IMAGE" version 2>/dev/null | grep -Fx "Pulse $PULSE_TAG" >/dev/null || { error "Pulse server version mismatch"; exit 1; }
|
||||||
success "Pulse server version: $PULSE_TAG"
|
success "Pulse server version: $PULSE_TAG"
|
||||||
|
|
||||||
# Host agent binary
|
# Host agent binary
|
||||||
docker run --rm --entrypoint /opt/pulse/bin/pulse-host-agent-linux-amd64 "$IMAGE" --version 2>/dev/null | grep -Fx "$PULSE_TAG" >/dev/null || { error "Host agent version mismatch"; exit 1; }
|
docker run --rm --entrypoint /opt/pulse/bin/pulse-host-agent-linux-amd64 "$IMAGE" --version 2>/dev/null | grep -Fx "$PULSE_TAG" >/dev/null || { error "Host agent version mismatch"; exit 1; }
|
||||||
success "Host agent version: $PULSE_TAG"
|
success "Host agent version: $PULSE_TAG"
|
||||||
|
|
||||||
# Sensor proxy binary
|
# Sensor proxy binary
|
||||||
docker run --rm --entrypoint /opt/pulse/bin/pulse-sensor-proxy-linux-amd64 "$IMAGE" version 2>/dev/null | grep -Fx "pulse-sensor-proxy $PULSE_TAG" >/dev/null || { error "Sensor proxy version mismatch"; exit 1; }
|
docker run --rm --entrypoint /opt/pulse/bin/pulse-sensor-proxy-linux-amd64 "$IMAGE" version 2>/dev/null | grep -Fx "pulse-sensor-proxy $PULSE_TAG" >/dev/null || { error "Sensor proxy version mismatch"; exit 1; }
|
||||||
success "Sensor proxy version: $PULSE_TAG"
|
success "Sensor proxy version: $PULSE_TAG"
|
||||||
|
|
||||||
# Docker agent binary (no CLI flag, check binary strings)
|
# Docker agent binary (no CLI flag, check binary strings)
|
||||||
docker run --rm --entrypoint /bin/sh -e EXPECTED_TAG="$PULSE_TAG" "$IMAGE" -c 'set -euo pipefail; grep -aF "$EXPECTED_TAG" /opt/pulse/bin/pulse-docker-agent-linux-amd64 >/dev/null' || { error "Docker agent version string not found"; exit 1; }
|
docker run --rm --entrypoint /bin/sh -e EXPECTED_TAG="$PULSE_TAG" "$IMAGE" -c 'set -euo pipefail; grep -aF "$EXPECTED_TAG" /opt/pulse/bin/pulse-docker-agent-linux-amd64 >/dev/null' || { error "Docker agent version string not found"; exit 1; }
|
||||||
success "Docker agent version embedded: $PULSE_TAG"
|
success "Docker agent version embedded: $PULSE_TAG"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
|
else
|
||||||
|
warn "=== Skipping Docker Image Validation (--skip-docker flag provided) ==="
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
#=============================================================================
|
#=============================================================================
|
||||||
# RELEASE TARBALL VALIDATION
|
# RELEASE TARBALL VALIDATION
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue