Files
Pulse/.github/workflows/create-release.yml
rcourtman e74b09557d fix: trigger Docker publish workflow in release pipeline
The release workflow publishes via GitHub API (patching draft to
published), which doesn't fire the release webhook. This meant the
Docker publish workflow was never triggered automatically.

Added explicit workflow dispatch for publish-docker.yml after release
publish, similar to how update-demo-server.yml was already dispatched.

Related to #797
2025-12-02 17:32:30 +00:00

499 lines
18 KiB
YAML

name: Pulse Release Pipeline
on:
workflow_dispatch:
inputs:
version:
description: 'Version number (e.g., 4.30.0)'
required: true
type: string
release_notes:
description: 'Release notes (markdown) - generated by Claude'
required: true
type: string
concurrency:
group: release-${{ github.event.inputs.version || github.ref || github.run_id }}
cancel-in-progress: false
jobs:
extract_version:
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
version: ${{ steps.extract.outputs.version }}
tag: ${{ steps.extract.outputs.tag }}
steps:
- name: Extract version
id: extract
run: |
# Handle both tag push and workflow_dispatch
if [ "${{ github.event_name }}" = "push" ]; then
TAG="${GITHUB_REF#refs/tags/}"
VERSION="${TAG#v}"
else
VERSION=$(jq -r '.inputs.version // ""' "$GITHUB_EVENT_PATH" 2>/dev/null || echo "")
if [ -z "$VERSION" ]; then
echo "::error::workflow_dispatch must include a version input"
exit 1
fi
TAG="v${VERSION}"
fi
echo "tag=${TAG}" >> $GITHUB_OUTPUT
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "Version: ${VERSION}, Tag: ${TAG}"
version_guard:
needs: extract_version
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Ensure VERSION file matches requested version
run: |
FILE_VERSION=$(cat VERSION | tr -d '\n')
REQUESTED_VERSION="${{ needs.extract_version.outputs.version }}"
if [ "$FILE_VERSION" != "$REQUESTED_VERSION" ]; then
echo "::error::VERSION file ($FILE_VERSION) does not match requested version ($REQUESTED_VERSION)."
echo "The VERSION file must be updated and committed before running release."
exit 1
fi
echo "✓ VERSION file matches requested version ($REQUESTED_VERSION)"
preflight_tests:
needs:
- extract_version
- version_guard
runs-on: ubuntu-latest
timeout-minutes: 90
permissions:
contents: read
packages: write
env:
FRONTEND_DIST: frontend-modern/dist
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: 'frontend-modern/package-lock.json'
- name: Install frontend dependencies
run: npm --prefix frontend-modern ci
- name: Restore frontend build cache
uses: actions/cache@v4
with:
path: frontend-modern/dist
key: frontend-build-${{ hashFiles('frontend-modern/package-lock.json', 'frontend-modern/src/**/*', 'frontend-modern/index.html', 'frontend-modern/postcss.config.cjs', 'frontend-modern/tailwind.config.cjs') }}
- name: Build frontend bundle for Go embed
run: |
if [ -d "$FRONTEND_DIST" ] && [ -f "$FRONTEND_DIST/index.html" ]; then
echo "Using cached frontend build";
else
npm --prefix frontend-modern run build;
fi
rm -rf internal/api/frontend-modern
mkdir -p internal/api/frontend-modern
cp -r frontend-modern/dist internal/api/frontend-modern/
- name: Lint frontend
run: npm --prefix frontend-modern run lint
- name: Install docker-compose
run: |
sudo apt-get update
sudo apt-get install -y docker-compose
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.24'
cache: true
- name: Run backend tests
run: make test
- name: Cache Playwright browsers
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('tests/integration/package-lock.json') }}
- name: Prepare integration test dependencies
working-directory: tests/integration
run: |
npm ci
npx playwright install --with-deps chromium
- name: Build Pulse for integration tests
run: make build
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push staging Docker images
uses: docker/build-push-action@v6
with:
context: .
target: runtime
platforms: linux/amd64,linux/arm64
push: true
provenance: false
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/pulse:buildcache
cache-to: type=registry,ref=ghcr.io/${{ github.repository_owner }}/pulse:buildcache,mode=max
tags: |
ghcr.io/${{ github.repository_owner }}/pulse:staging-${{ needs.extract_version.outputs.tag }}
- name: Build and push staging Docker agent image
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
target: agent_runtime
platforms: linux/amd64,linux/arm64
push: true
provenance: false
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/pulse-docker-agent:buildcache
cache-to: type=registry,ref=ghcr.io/${{ github.repository_owner }}/pulse-docker-agent:buildcache,mode=max
tags: |
ghcr.io/${{ github.repository_owner }}/pulse-docker-agent:staging-${{ needs.extract_version.outputs.tag }}
- name: Build Docker images for integration tests
run: |
docker build -t pulse-mock-github:test tests/integration/mock-github-server
docker build -t pulse:test -f Dockerfile --target runtime --cache-from ghcr.io/${{ github.repository_owner }}/pulse:buildcache --build-arg BUILDKIT_INLINE_CACHE=1 .
- name: Run update integration smoke tests
working-directory: tests/integration
env:
MOCK_CHECKSUM_ERROR: "false"
MOCK_NETWORK_ERROR: "false"
MOCK_RATE_LIMIT: "false"
MOCK_STALE_RELEASE: "false"
run: |
docker-compose -f docker-compose.test.yml up -d
# Wait for services to be healthy
echo "Waiting for mock-github to be healthy..."
timeout 60 sh -c 'until docker inspect --format="{{json .State.Health.Status}}" pulse-mock-github | grep -q "healthy"; do sleep 2; done' || {
echo "Mock GitHub failed to become healthy"
docker logs pulse-mock-github
exit 1
}
echo "Waiting for pulse-test-server to be healthy..."
timeout 60 sh -c 'until docker inspect --format="{{json .State.Health.Status}}" pulse-test-server | grep -q "healthy"; do sleep 2; done' || {
echo "Pulse server failed to become healthy"
docker logs pulse-test-server
exit 1
}
echo "All services healthy, verifying port mapping..."
# Test that the host can actually reach the container through port mapping
for i in 1 2 3 4 5; do
if curl -f -s http://localhost:7655/api/health > /dev/null 2>&1; then
echo "Port mapping verified: Pulse server is reachable from host"
break
elif [ $i -eq 5 ]; then
echo "ERROR: Port mapping failed - cannot reach Pulse server from host"
echo "Container healthcheck passed, but host cannot connect via localhost:7655"
echo "Pulse server logs:"
docker logs pulse-test-server || true
echo "Mock GitHub logs:"
docker logs pulse-mock-github || true
exit 1
else
echo "Attempt $i: Server not yet reachable from host, waiting..."
sleep 2
fi
done
echo "Running API-level update integration test..."
UPDATE_API_BASE_URL=http://localhost:7655 go test ../../tests/integration/api -run TestUpdateFlowIntegration -count=1
echo "Skipping legacy Playwright update scenarios (removed until they can be rebuilt)"
docker-compose -f docker-compose.test.yml down -v
- name: Cleanup integration environment
if: always()
working-directory: tests/integration
run: docker-compose -f docker-compose.test.yml down -v || true
create_release:
needs:
- extract_version
- preflight_tests
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: write
outputs:
release_id: ${{ steps.create_release.outputs.release_id }}
release_url: ${{ steps.create_release.outputs.release_url }}
target_commitish: ${{ github.sha }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch all history for changelog generation
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.24'
cache: true
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: 'frontend-modern/package-lock.json'
- name: Install dependencies
run: |
# Install zip for Windows binaries
sudo apt-get update
sudo apt-get install -y zip
- name: Set up Helm
uses: azure/setup-helm@v4
with:
version: 'v3.15.2'
- name: Build release artifacts
run: |
echo "Building release ${{ needs.extract_version.outputs.tag }}..."
./scripts/build-release.sh ${{ needs.extract_version.outputs.version }}
- name: Post-build health check
run: |
echo "Verifying server binary responds to --version and API health..."
if [ -x ./pulse ]; then
./pulse --version
elif [ -x ./cmd/pulse/pulse ]; then
./cmd/pulse/pulse --version
else
echo "::warning::Pulse binary not found after build-release; skipping version check"
fi
- name: Prepare release notes
id: generate_notes
run: |
VERSION="${{ needs.extract_version.outputs.version }}"
RELEASE_NOTES_INPUT=$(jq -r '.inputs.release_notes // ""' "$GITHUB_EVENT_PATH" 2>/dev/null || echo "")
# Save release notes to file
NOTES_FILE=$(mktemp)
if [ -n "$RELEASE_NOTES_INPUT" ]; then
echo "Using Claude-generated release notes from workflow input"
printf "%s\n" "$RELEASE_NOTES_INPUT" > "$NOTES_FILE"
else
echo "Tag-triggered release - using placeholder notes"
echo "Release $VERSION" > "$NOTES_FILE"
echo "" >> "$NOTES_FILE"
echo "See commit history for changes." >> "$NOTES_FILE"
fi
# Add installation instructions
{
echo ""
echo "## Installation"
echo ""
echo "**Docker (recommended):**"
echo '```bash'
echo "docker pull rcourtman/pulse:${VERSION}"
echo '```'
echo ""
echo "**Docker Compose:**"
echo "Update your \`docker-compose.yml\` to use \`rcourtman/pulse:${VERSION}\`"
echo ""
echo "See the [Installation Guide](https://github.com/rcourtman/Pulse#installation) for complete setup instructions."
} >> "$NOTES_FILE"
echo "notes_file=${NOTES_FILE}" >> $GITHUB_OUTPUT
echo "Release notes content:"
cat "$NOTES_FILE"
- name: Create tag
env:
GH_TOKEN: ${{ github.token }}
run: |
TAG="${{ needs.extract_version.outputs.tag }}"
echo "Creating tag ${TAG}..."
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag -a "${TAG}" -m "Release ${TAG}"
git push origin "${TAG}"
- name: Create draft release
id: create_release
env:
GH_TOKEN: ${{ github.token }}
run: |
TAG="${{ needs.extract_version.outputs.tag }}"
NOTES_FILE="${{ steps.generate_notes.outputs.notes_file }}"
echo "Creating draft release for ${TAG}..."
# Tag must exist first - draft releases can't create tags (GitHub API limitation)
# See: https://github.com/cli/cli/issues/11589
# Create as draft first so we can upload assets before publishing
RELEASE_JSON=$(gh api "repos/${{ github.repository }}/releases" \
-X POST \
-F tag_name="${TAG}" \
-F name="Pulse ${TAG}" \
-F body="$(cat $NOTES_FILE)" \
-F draft=true)
rm -f "$NOTES_FILE"
RELEASE_ID=$(echo "$RELEASE_JSON" | jq -r '.id')
RELEASE_URL=$(echo "$RELEASE_JSON" | jq -r '.html_url')
if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" = "null" ]; then
echo "::error::Failed to extract release ID from API response"
exit 1
fi
echo "release_url=${RELEASE_URL}" >> $GITHUB_OUTPUT
echo "release_id=${RELEASE_ID}" >> $GITHUB_OUTPUT
echo "✓ Draft release created: ${TAG} (ID: ${RELEASE_ID})"
- name: Upload checksums.txt
env:
GH_TOKEN: ${{ github.token }}
run: |
TAG="${{ needs.extract_version.outputs.tag }}"
echo "Uploading checksums.txt..."
gh release upload "${TAG}" release/checksums.txt
# Upload individual .sha256 files for backward compatibility
echo "Uploading .sha256 checksum files..."
gh release upload "${TAG}" release/*.sha256
- name: Upload release assets
env:
GH_TOKEN: ${{ github.token }}
run: |
TAG="${{ needs.extract_version.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 scripts as standalone assets
# Users can now use: curl -fsSL https://github.com/rcourtman/Pulse/releases/latest/download/install.sh | bash
# This ensures scripts are version-locked to the release, not pulled from main branch
gh release upload "${TAG}" release/install.sh
gh release upload "${TAG}" release/install-sensor-proxy.sh
gh release upload "${TAG}" release/install-docker.sh
gh release upload "${TAG}" release/pulse-auto-update.sh
- name: Publish release
env:
GH_TOKEN: ${{ github.token }}
run: |
TAG="${{ needs.extract_version.outputs.tag }}"
RELEASE_ID="${{ steps.create_release.outputs.release_id }}"
echo "Publishing release ${TAG}..."
gh api "repos/${{ github.repository }}/releases/${RELEASE_ID}" \
-X PATCH \
-F draft=false \
-F make_latest=true
echo "✓ Release published as latest: ${TAG}"
- name: Trigger Docker image publish
continue-on-error: true # Non-fatal if dispatch fails
env:
GH_TOKEN: ${{ secrets.WORKFLOW_PAT }}
run: |
TAG="${{ needs.extract_version.outputs.tag }}"
echo "Triggering Docker image publish for ${TAG}..."
# Publishing via API doesn't fire the release webhook, so we dispatch manually
# Requires WORKFLOW_PAT secret with 'repo' and 'workflow' scopes
gh workflow run publish-docker.yml -f tag="${TAG}"
echo "✓ Docker publish workflow dispatched"
- name: Trigger demo server update
continue-on-error: true # Non-fatal if dispatch fails
env:
GH_TOKEN: ${{ secrets.WORKFLOW_PAT }}
run: |
TAG="${{ needs.extract_version.outputs.tag }}"
echo "Triggering demo server update for ${TAG}..."
gh workflow run update-demo-server.yml -f tag="${TAG}"
echo "✓ Demo server update workflow dispatched"
- name: Output release information
run: |
echo "✅ Release published successfully!"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "📦 Release: ${{ needs.extract_version.outputs.tag }}"
echo "🔗 URL: ${{ steps.create_release.outputs.release_url }}"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "Docker images, Helm chart, and demo server will be updated automatically."
echo ""
validate_release_assets:
needs:
- extract_version
- create_release
uses: ./.github/workflows/validate-release-assets.yml
secrets: inherit
with:
tag: ${{ needs.extract_version.outputs.tag }}
version: ${{ needs.extract_version.outputs.version }}
release_id: ${{ needs.create_release.outputs.release_id }}
draft: false
target_commitish: ${{ needs.create_release.outputs.target_commitish }}