Workflow now checks for annotated tags and uses the annotation as release notes. If no annotation exists, falls back to GitHub's auto-generation. This allows Claude to write formatted release notes when creating releases, stored directly in git history as part of the tag.
501 lines
18 KiB
YAML
501 lines
18 KiB
YAML
name: Release
|
|
|
|
on:
|
|
push:
|
|
tags:
|
|
- 'v*.*.*'
|
|
|
|
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 from tag
|
|
id: extract
|
|
run: |
|
|
TAG="${GITHUB_REF#refs/tags/}"
|
|
VERSION="${TAG#v}"
|
|
echo "tag=${TAG}" >> $GITHUB_OUTPUT
|
|
echo "version=${VERSION}" >> $GITHUB_OUTPUT
|
|
echo "Extracted version: ${VERSION} from 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 tag
|
|
run: |
|
|
FILE_VERSION=$(cat VERSION | tr -d '\n')
|
|
TAG_VERSION="${{ needs.extract-version.outputs.version }}"
|
|
if [ "$FILE_VERSION" != "$TAG_VERSION" ]; then
|
|
echo "::error::VERSION file ($FILE_VERSION) does not match tag version ($TAG_VERSION)."
|
|
echo "The VERSION file must be updated and committed before pushing the tag."
|
|
exit 1
|
|
fi
|
|
echo "✓ VERSION file matches tag ($TAG_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 Playwright diagnostics..."
|
|
npx playwright test tests/00-diagnostic.spec.ts --reporter=list
|
|
|
|
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
|
|
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
|
|
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: Generate release notes
|
|
id: generate_notes
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
TAG="${{ needs.extract-version.outputs.tag }}"
|
|
VERSION="${{ needs.extract-version.outputs.version }}"
|
|
|
|
echo "Generating release notes for ${TAG}..."
|
|
|
|
# Check if tag has an annotation (release notes from Claude)
|
|
TAG_MESSAGE=$(git tag -l --format='%(contents)' "${TAG}")
|
|
|
|
NOTES_FILE=$(mktemp)
|
|
|
|
if [ -n "$TAG_MESSAGE" ] && [ "$TAG_MESSAGE" != "" ]; then
|
|
echo "Using release notes from tag annotation"
|
|
echo "$TAG_MESSAGE" > "$NOTES_FILE"
|
|
else
|
|
echo "No tag annotation found, generating release notes from commits..."
|
|
|
|
# Get previous tag
|
|
PREV_TAG=$(git tag --sort=-version:refname | grep -A1 "^${TAG}$" | tail -1)
|
|
|
|
if [ -z "$PREV_TAG" ] || [ "$PREV_TAG" = "$TAG" ]; then
|
|
echo "No previous tag found, using initial commit"
|
|
PREV_TAG=$(git rev-list --max-parents=0 HEAD)
|
|
fi
|
|
|
|
echo "Previous release: ${PREV_TAG}"
|
|
|
|
# Use GitHub's API to generate release notes
|
|
NOTES_JSON=$(gh api \
|
|
--method POST \
|
|
-H "Accept: application/vnd.github+json" \
|
|
"/repos/${{ github.repository }}/releases/generate-notes" \
|
|
-f tag_name="${TAG}" \
|
|
-f target_commitish="${{ github.sha }}" \
|
|
-f previous_tag_name="${PREV_TAG}")
|
|
|
|
# Extract the generated body
|
|
GENERATED_NOTES=$(echo "$NOTES_JSON" | jq -r '.body')
|
|
echo "$GENERATED_NOTES" > "$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 "Generated release notes:"
|
|
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 }}
|