Pulse/.github/workflows/release.yml
rcourtman 029c19c9ec Add Playwright diagnostic test to check browser API access
Created test that:
- Navigates to /login in actual browser context
- Fetches /api/security/status from browser JavaScript
- Checks if username field appears
- Captures screenshot and page content if field missing

This will reveal if browser can access API and what response it gets.

Related to #695
2025-11-12 10:43:34 +00:00

476 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 "Testing API access from Playwright context..."
cat > test-api-access.spec.ts <<'EOF'
import { test, expect } from '@playwright/test';
test('API is accessible from browser', async ({ page }) => {
console.log('Navigating to login page...');
await page.goto('http://localhost:7655/login');
console.log('Page loaded');
console.log('Fetching security status from browser context...');
const response = await page.evaluate(async () => {
try {
const res = await fetch('/api/security/status');
const data = await res.json();
return { ok: res.ok, status: res.status, data };
} catch (err) {
return { error: String(err) };
}
});
console.log('Security status from browser:', JSON.stringify(response, null, 2));
console.log('Checking for username field...');
const usernameField = page.locator('input[name="username"]');
const isVisible = await usernameField.isVisible({ timeout: 15000 }).catch(() => false);
console.log('Username field visible:', isVisible);
if (!isVisible) {
console.log('Taking screenshot of what IS visible...');
await page.screenshot({ path: '/tmp/login-page.png', fullPage: true });
const bodyText = await page.locator('body').textContent();
console.log('Page text content:', bodyText);
}
});
EOF
npx playwright test test-api-access.spec.ts --reporter=list || true
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"
echo "release_url=$(gh release view ${TAG} --json url -q .url)" >> $GITHUB_OUTPUT
echo "release_id=$(gh release view ${TAG} --json id -q .id)" >> $GITHUB_OUTPUT
echo "tag=${TAG}" >> $GITHUB_OUTPUT
- 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 "Automated validation will now verify the release assets."
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 }}