The releases REST API endpoint is eventually consistent for draft releases. Immediately after gh release create, the new release may not appear in the listing yet, causing the release_id lookup to return empty and fail validation. Add retry loop (10 attempts, 2s intervals) to wait for the release to appear in the API before extracting the ID. Also add validation to ensure we got a valid release_id before proceeding. This fixes the immediate validation failure with 'Release metadata is missing'.
489 lines
18 KiB
YAML
489 lines
18 KiB
YAML
name: Release
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
version:
|
|
description: 'Version number (e.g., 4.28.1)'
|
|
required: true
|
|
type: string
|
|
|
|
jobs:
|
|
version_guard:
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 10
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Ensure VERSION matches requested release
|
|
run: |
|
|
FILE_VERSION=$(cat VERSION | tr -d '\n')
|
|
REQUESTED_VERSION="${{ inputs.version }}"
|
|
if [ "$FILE_VERSION" != "$REQUESTED_VERSION" ]; then
|
|
echo "::error::VERSION file ($FILE_VERSION) does not match requested version ($REQUESTED_VERSION)."
|
|
echo "Update the VERSION file and commit the change before running the release workflow."
|
|
exit 1
|
|
fi
|
|
echo "VERSION file matches requested release ($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'
|
|
|
|
- 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'
|
|
|
|
- name: Run backend tests
|
|
run: go test ./...
|
|
|
|
- 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 "Diagnosing authentication configuration..."
|
|
echo "Environment variables in container:"
|
|
docker exec pulse-test-server env | grep PULSE_AUTH || echo "No PULSE_AUTH variables found"
|
|
echo "Security status endpoint from host:"
|
|
curl -s http://localhost:7655/api/security/status | jq . || echo "Failed to query security status"
|
|
echo "Security status endpoint from inside container:"
|
|
docker exec pulse-test-server wget -q -O- http://localhost:7655/api/security/status || echo "Failed from inside"
|
|
echo "Container startup logs (looking for auth messages):"
|
|
docker logs pulse-test-server 2>&1 | grep -i "auth\|overriding\|hashed" || echo "No auth-related messages in logs"
|
|
echo "Testing login page loads:"
|
|
curl -s http://localhost:7655/login | head -20
|
|
|
|
echo "Running diagnostic test..."
|
|
npx playwright test tests/00-diagnostic.spec.ts --reporter=list
|
|
|
|
echo "Skipping integration tests temporarily (issue with embedded frontend not rendering)"
|
|
echo "These tests were added Nov 11, 2025 and have never passed"
|
|
echo "Will be fixed separately - not blocking release"
|
|
# echo "Running integration tests..."
|
|
# npx playwright test tests/01-happy-path.spec.ts tests/02-bad-checksums.spec.ts --reporter=list
|
|
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:
|
|
- version_guard
|
|
- preflight-tests
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 60
|
|
permissions:
|
|
contents: read
|
|
packages: write
|
|
outputs:
|
|
tag: ${{ steps.set_version.outputs.tag }}
|
|
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Set version output
|
|
id: set_version
|
|
run: |
|
|
VERSION="${{ inputs.version }}"
|
|
TAG="v${VERSION}"
|
|
echo "tag=${TAG}" >> $GITHUB_OUTPUT
|
|
echo "Building Docker images for ${TAG}..."
|
|
|
|
- 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
|
|
tags: |
|
|
rcourtman/pulse:${{ steps.set_version.outputs.tag }}
|
|
rcourtman/pulse:${{ inputs.version }}
|
|
ghcr.io/${{ github.repository_owner }}/pulse:${{ steps.set_version.outputs.tag }}
|
|
ghcr.io/${{ github.repository_owner }}/pulse:${{ inputs.version }}
|
|
labels: |
|
|
org.opencontainers.image.title=Pulse
|
|
org.opencontainers.image.description=Proxmox monitoring system
|
|
org.opencontainers.image.version=${{ steps.set_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
|
|
tags: |
|
|
rcourtman/pulse-docker-agent:${{ steps.set_version.outputs.tag }}
|
|
rcourtman/pulse-docker-agent:${{ inputs.version }}
|
|
ghcr.io/${{ github.repository_owner }}/pulse-docker-agent:${{ steps.set_version.outputs.tag }}
|
|
ghcr.io/${{ github.repository_owner }}/pulse-docker-agent:${{ inputs.version }}
|
|
labels: |
|
|
org.opencontainers.image.title=Pulse Docker Agent
|
|
org.opencontainers.image.description=Docker container monitoring agent for Pulse
|
|
org.opencontainers.image.version=${{ steps.set_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:${{ steps.set_version.outputs.tag }}"
|
|
echo " - rcourtman/pulse:${{ inputs.version }}"
|
|
echo ""
|
|
echo "Agent images:"
|
|
echo " - rcourtman/pulse-docker-agent:${{ steps.set_version.outputs.tag }}"
|
|
echo " - rcourtman/pulse-docker-agent:${{ inputs.version }}"
|
|
|
|
create-release:
|
|
needs: build-docker-images
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 30
|
|
permissions:
|
|
contents: write
|
|
outputs:
|
|
tag: ${{ steps.create_release.outputs.tag }}
|
|
release_id: ${{ steps.create_release.outputs.release_id }}
|
|
release_url: ${{ steps.create_release.outputs.release_url }}
|
|
target_commitish: ${{ steps.commit_metadata.outputs.commitish }}
|
|
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0 # Fetch all history for tags
|
|
|
|
- name: Fetch all tags
|
|
run: git fetch --tags --force
|
|
|
|
- name: Capture commit metadata
|
|
id: commit_metadata
|
|
run: echo "commitish=$GITHUB_SHA" >> $GITHUB_OUTPUT
|
|
|
|
- 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: Generate release notes
|
|
id: generate_notes
|
|
env:
|
|
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
|
run: |
|
|
echo "Generating release notes using LLM..."
|
|
|
|
# Find previous release tag (simply get the latest tag)
|
|
PREVIOUS_TAG=$(git tag --sort=-version:refname | head -1)
|
|
|
|
if [ -z "$PREVIOUS_TAG" ]; then
|
|
echo "No previous tag found, comparing with first commit"
|
|
PREVIOUS_TAG=$(git rev-list --max-parents=0 HEAD)
|
|
fi
|
|
|
|
echo "Comparing v${{ inputs.version }} with ${PREVIOUS_TAG}..."
|
|
|
|
# Generate notes - script writes output between separator lines
|
|
TEMP_OUTPUT=$(mktemp)
|
|
|
|
# Run script, capture exit code (don't fail immediately)
|
|
set +e
|
|
./scripts/generate-release-notes.sh ${{ inputs.version }} "${PREVIOUS_TAG}" > "$TEMP_OUTPUT" 2>&1
|
|
SCRIPT_EXIT=$?
|
|
set -e
|
|
|
|
if [ $SCRIPT_EXIT -ne 0 ]; then
|
|
echo "❌ Release notes generation failed:"
|
|
cat "$TEMP_OUTPUT"
|
|
rm -f "$TEMP_OUTPUT"
|
|
exit 1
|
|
fi
|
|
|
|
# Extract notes between separator lines (everything between the two ━━━ lines)
|
|
RELEASE_NOTES=$(awk '/^Generated release notes:/{flag=1;next}/^━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━$/{if(flag==1){flag=2}else if(flag==2){flag=3}}flag==2' "$TEMP_OUTPUT")
|
|
|
|
rm -f "$TEMP_OUTPUT"
|
|
|
|
# Save to output (escape for GitHub Actions multiline)
|
|
echo "RELEASE_NOTES<<EOF" >> $GITHUB_OUTPUT
|
|
echo "$RELEASE_NOTES" >> $GITHUB_OUTPUT
|
|
echo "EOF" >> $GITHUB_OUTPUT
|
|
|
|
echo "✅ Release notes generated successfully"
|
|
|
|
- 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}..."
|
|
|
|
# Write release notes directly from GitHub output to file
|
|
# This avoids bash interpreting backticks in the markdown
|
|
NOTES_FILE=$(mktemp)
|
|
cat <<'NOTES_EOF' > "$NOTES_FILE"
|
|
${{ steps.generate_notes.outputs.RELEASE_NOTES }}
|
|
NOTES_EOF
|
|
|
|
# Create draft release with generated notes
|
|
gh release create "${TAG}" \
|
|
--draft \
|
|
--title "Pulse ${TAG}" \
|
|
--notes-file "$NOTES_FILE"
|
|
|
|
rm -f "$NOTES_FILE"
|
|
|
|
# Get the numeric release ID from the REST API (not GraphQL node_id)
|
|
# The validation workflow needs the numeric ID for REST API calls
|
|
# Note: /releases/tags/{tag} endpoint doesn't work for draft releases,
|
|
# so we list all releases and filter by tag_name
|
|
# The releases endpoint is eventually consistent, so retry until the release appears
|
|
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 "tag=${TAG}" >> $GITHUB_OUTPUT
|
|
|
|
echo "✓ Release ID: ${RELEASE_ID}"
|
|
|
|
- name: Upload checksums.txt
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
TAG="${{ steps.create_release.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="${{ 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
|
|
|
|
- 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."
|
|
echo "Docker images are available at Docker Hub and GHCR."
|
|
echo ""
|
|
echo "✅ Release workflow completed successfully!"
|
|
echo "Review the draft release and publish when ready."
|
|
echo ""
|
|
|
|
validate-release-assets:
|
|
needs: create-release
|
|
uses: ./.github/workflows/validate-release-assets.yml
|
|
secrets: inherit
|
|
with:
|
|
tag: ${{ needs.create-release.outputs.tag }}
|
|
version: ${{ inputs.version }}
|
|
release_id: ${{ needs.create-release.outputs.release_id }}
|
|
draft: true
|
|
target_commitish: ${{ needs.create-release.outputs.target_commitish }}
|