From 4e7efa57c3cdd90bc3ff1e0e905261d41c08af67 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Thu, 13 Nov 2025 23:23:39 +0000 Subject: [PATCH] Add new pulse-release workflow with unique name --- .github/workflows/pulse-release.yml | 490 ++++++++++++++++++++++++++++ 1 file changed, 490 insertions(+) create mode 100644 .github/workflows/pulse-release.yml diff --git a/.github/workflows/pulse-release.yml b/.github/workflows/pulse-release.yml new file mode 100644 index 000000000..c536c77c3 --- /dev/null +++ b/.github/workflows/pulse-release.yml @@ -0,0 +1,490 @@ +name: Pulse Release Pipeline +# Triggers: workflow_dispatch and tag push +on: + push: + tags: + - 'v*.*.*' + 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: false + type: string + +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="${{ inputs.version }}" + 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: version-guard + runs-on: ubuntu-latest + timeout-minutes: 90 + 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: Build frontend bundle for Go embed + run: | + npm --prefix frontend-modern run build + 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: go 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: 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 . + + - 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 + + build-docker-images: + needs: + - extract-version + - version-guard + - preflight-tests + runs-on: ubuntu-latest + timeout-minutes: 60 + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - 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 Pulse server image + uses: docker/build-push-action@v6 + with: + context: . + 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: | + rcourtman/pulse:${{ needs.extract-version.outputs.tag }} + rcourtman/pulse:${{ needs.extract-version.outputs.version }} + rcourtman/pulse:latest + ghcr.io/${{ github.repository_owner }}/pulse:${{ needs.extract-version.outputs.tag }} + ghcr.io/${{ github.repository_owner }}/pulse:${{ needs.extract-version.outputs.version }} + ghcr.io/${{ github.repository_owner }}/pulse:latest + labels: | + org.opencontainers.image.title=Pulse + org.opencontainers.image.description=Proxmox monitoring system + org.opencontainers.image.version=${{ needs.extract-version.outputs.tag }} + org.opencontainers.image.created=${{ github.event.repository.updated_at }} + org.opencontainers.image.revision=${{ github.sha }} + org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} + org.opencontainers.image.url=${{ github.server_url }}/${{ github.repository }} + org.opencontainers.image.licenses=MIT + + - name: Build and push 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: | + rcourtman/pulse-docker-agent:${{ needs.extract-version.outputs.tag }} + rcourtman/pulse-docker-agent:${{ needs.extract-version.outputs.version }} + rcourtman/pulse-docker-agent:latest + ghcr.io/${{ github.repository_owner }}/pulse-docker-agent:${{ needs.extract-version.outputs.tag }} + ghcr.io/${{ github.repository_owner }}/pulse-docker-agent:${{ needs.extract-version.outputs.version }} + ghcr.io/${{ github.repository_owner }}/pulse-docker-agent:latest + labels: | + org.opencontainers.image.title=Pulse Docker Agent + org.opencontainers.image.description=Docker container monitoring agent for Pulse + org.opencontainers.image.version=${{ needs.extract-version.outputs.tag }} + org.opencontainers.image.created=${{ github.event.repository.updated_at }} + org.opencontainers.image.revision=${{ github.sha }} + org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} + org.opencontainers.image.url=${{ github.server_url }}/${{ github.repository }} + org.opencontainers.image.licenses=MIT + + - name: Output Docker image information + run: | + echo "✅ Docker images built and pushed successfully!" + echo "" + echo "Server images:" + echo " - rcourtman/pulse:${{ needs.extract-version.outputs.tag }}" + echo " - rcourtman/pulse:${{ needs.extract-version.outputs.version }}" + echo " - rcourtman/pulse:latest" + echo "" + echo "Agent images:" + echo " - rcourtman/pulse-docker-agent:${{ needs.extract-version.outputs.tag }}" + echo " - rcourtman/pulse-docker-agent:${{ needs.extract-version.outputs.version }}" + echo " - rcourtman/pulse-docker-agent:latest" + + create-release: + needs: + - extract-version + - build-docker-images + 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' + + - 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 ${{ needs.extract-version.outputs.tag }}..." + ./scripts/build-release.sh ${{ needs.extract-version.outputs.version }} + + - name: Prepare release notes + id: generate_notes + env: + RELEASE_NOTES_INPUT: ${{ inputs.release_notes }} + run: | + VERSION="${{ needs.extract-version.outputs.version }}" + + # 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 + cat >> "$NOTES_FILE" << EOF + +## Installation + +**Docker (recommended):** +\`\`\`bash +docker pull rcourtman/pulse:${VERSION} +\`\`\` + +**Docker Compose:** +Update your \`docker-compose.yml\` to use \`rcourtman/pulse:${VERSION}\` + +See the [Installation Guide](https://github.com/rcourtman/Pulse#installation) for complete setup instructions. +EOF + + echo "notes_file=${NOTES_FILE}" >> $GITHUB_OUTPUT + + echo "Release notes content:" + cat "$NOTES_FILE" + + - 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 already exists (pushed by user), so don't create it + # Just create the release pointing to the existing tag + gh release create "${TAG}" \ + --draft \ + --title "Pulse ${TAG}" \ + --notes-file "$NOTES_FILE" + + rm -f "$NOTES_FILE" + + # Get the release ID + echo "Waiting for release to appear in GitHub API..." + MAX_ATTEMPTS=10 + ATTEMPT=1 + RELEASE_JSON="" + + while [ $ATTEMPT -le $MAX_ATTEMPTS ]; do + echo "Attempt $ATTEMPT/$MAX_ATTEMPTS: Looking for release ${TAG}..." + RELEASE_JSON=$(gh api "repos/${{ github.repository }}/releases" --paginate | jq ".[] | select(.tag_name == \"${TAG}\")") + + if [ -n "$RELEASE_JSON" ]; then + echo "✓ Found release in API" + break + fi + + if [ $ATTEMPT -lt $MAX_ATTEMPTS ]; then + echo "Release not indexed yet, waiting 2 seconds..." + sleep 2 + fi + + ATTEMPT=$((ATTEMPT + 1)) + done + + if [ -z "$RELEASE_JSON" ]; then + echo "::error::Failed to find release ${TAG} in API after $MAX_ATTEMPTS attempts" + exit 1 + fi + + RELEASE_ID=$(echo "$RELEASE_JSON" | jq -r '.id') + + if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" = "null" ]; then + echo "::error::Failed to extract release ID from API response" + exit 1 + fi + + echo "release_url=$(echo "$RELEASE_JSON" | jq -r '.html_url')" >> $GITHUB_OUTPUT + echo "release_id=${RELEASE_ID}" >> $GITHUB_OUTPUT + + echo "✓ Release 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.sh + gh release upload "${TAG}" release/install.sh + + - name: Output release information + run: | + echo "✅ Release draft created successfully!" + echo "" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "📦 Release: ${{ needs.extract-version.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 automatically generated release notes" + echo "2. Edit and categorize changes as needed" + echo "3. Publish the release when ready" + echo "" + echo "All artifacts have been uploaded." + echo "Docker images are available at Docker Hub and GHCR." + 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: true + target_commitish: ${{ needs.create-release.outputs.target_commitish }}