mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-05 23:36:37 +00:00
701 lines
27 KiB
YAML
701 lines
27 KiB
YAML
name: Pulse Release Pipeline
|
|
# Optimized: parallel jobs, fast prerelease path
|
|
|
|
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
|
|
promoted_from_tag:
|
|
description: 'Stable only: prerelease tag being promoted (for example 6.0.0-rc.2)'
|
|
required: false
|
|
type: string
|
|
rollback_version:
|
|
description: 'Required: prior stable version to pin for rollback (for example 5.1.14 or v5.1.14)'
|
|
required: true
|
|
type: string
|
|
ga_date:
|
|
description: 'First stable v6.0.0 GA only: exact GA publish date (YYYY-MM-DD)'
|
|
required: false
|
|
type: string
|
|
v5_eos_date:
|
|
description: 'First stable v6.0.0 GA only: Pulse v5 end-of-support date (YYYY-MM-DD)'
|
|
required: false
|
|
type: string
|
|
hotfix_exception:
|
|
description: 'Stable only: bypass the 72-hour prerelease soak for urgent customer harm'
|
|
required: false
|
|
type: boolean
|
|
default: false
|
|
hotfix_reason:
|
|
description: 'Stable only: reason for hotfix soak exception'
|
|
required: false
|
|
type: string
|
|
draft_only:
|
|
description: 'Create draft release only (do not publish)'
|
|
required: false
|
|
type: boolean
|
|
default: false
|
|
|
|
concurrency:
|
|
group: release-${{ github.event.inputs.version || github.ref || github.run_id }}
|
|
cancel-in-progress: false
|
|
|
|
jobs:
|
|
# Combined version extraction and validation (saves a checkout)
|
|
prepare:
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
outputs:
|
|
version: ${{ steps.extract.outputs.version }}
|
|
tag: ${{ steps.extract.outputs.tag }}
|
|
is_prerelease: ${{ steps.extract.outputs.is_prerelease }}
|
|
source_branch: ${{ steps.extract.outputs.source_branch }}
|
|
required_branch: ${{ steps.branch_policy.outputs.required_branch }}
|
|
promoted_from_tag: ${{ steps.promotion.outputs.promoted_from_tag }}
|
|
rollback_tag: ${{ steps.promotion.outputs.rollback_tag }}
|
|
rollback_command: ${{ steps.promotion.outputs.rollback_command }}
|
|
ga_date: ${{ steps.promotion.outputs.ga_date }}
|
|
v5_eos_date: ${{ steps.promotion.outputs.v5_eos_date }}
|
|
hotfix_exception: ${{ steps.promotion.outputs.hotfix_exception }}
|
|
hotfix_reason: ${{ steps.promotion.outputs.hotfix_reason }}
|
|
steps:
|
|
- name: Extract version
|
|
id: extract
|
|
run: |
|
|
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}"
|
|
|
|
IS_PRERELEASE="false"
|
|
if [[ "$VERSION" =~ -rc\.[0-9]+$ ]] || [[ "$VERSION" =~ -alpha\.[0-9]+$ ]] || [[ "$VERSION" =~ -beta\.[0-9]+$ ]]; then
|
|
IS_PRERELEASE="true"
|
|
echo "Detected prerelease version: ${VERSION}"
|
|
fi
|
|
|
|
if [[ "${GITHUB_REF}" != refs/heads/* ]]; then
|
|
echo "::error::Release workflow must be dispatched from a branch ref (current ref: ${GITHUB_REF})."
|
|
exit 1
|
|
fi
|
|
|
|
SOURCE_BRANCH="${GITHUB_REF_NAME}"
|
|
echo "tag=${TAG}" >> $GITHUB_OUTPUT
|
|
echo "version=${VERSION}" >> $GITHUB_OUTPUT
|
|
echo "is_prerelease=${IS_PRERELEASE}" >> $GITHUB_OUTPUT
|
|
echo "source_branch=${SOURCE_BRANCH}" >> $GITHUB_OUTPUT
|
|
echo "Version: ${VERSION}, Tag: ${TAG}, Prerelease: ${IS_PRERELEASE}, Branch: ${SOURCE_BRANCH}"
|
|
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0
|
|
sparse-checkout: |
|
|
VERSION
|
|
docs/release-control/control_plane.json
|
|
scripts/release_control/control_plane.py
|
|
scripts/release_control/repo_file_io.py
|
|
|
|
- name: Resolve required release branch
|
|
id: branch_policy
|
|
run: |
|
|
REQUIRED_BRANCH="$(python3 scripts/release_control/control_plane.py --branch-for-version "${{ steps.extract.outputs.version }}")"
|
|
if [ "${{ steps.extract.outputs.source_branch }}" != "$REQUIRED_BRANCH" ]; then
|
|
echo "::error::Invalid release line. Version ${{ steps.extract.outputs.version }} must run from ${REQUIRED_BRANCH}, but workflow ref is ${{ steps.extract.outputs.source_branch }}."
|
|
exit 1
|
|
fi
|
|
echo "required_branch=${REQUIRED_BRANCH}" >> "$GITHUB_OUTPUT"
|
|
echo "[OK] Governed release branch for ${{ steps.extract.outputs.version }} is ${REQUIRED_BRANCH}"
|
|
|
|
- name: Validate VERSION file
|
|
run: |
|
|
FILE_VERSION=$(cat VERSION | tr -d '\n')
|
|
REQUESTED_VERSION="${{ steps.extract.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 "[OK] VERSION file matches requested version ($REQUESTED_VERSION)"
|
|
|
|
- name: Validate promotion policy
|
|
id: promotion
|
|
env:
|
|
VERSION: ${{ steps.extract.outputs.version }}
|
|
TAG: ${{ steps.extract.outputs.tag }}
|
|
IS_PRERELEASE: ${{ steps.extract.outputs.is_prerelease }}
|
|
PROMOTED_FROM_TAG_INPUT: ${{ github.event.inputs.promoted_from_tag }}
|
|
ROLLBACK_VERSION_INPUT: ${{ github.event.inputs.rollback_version }}
|
|
GA_DATE_INPUT: ${{ github.event.inputs.ga_date }}
|
|
V5_EOS_DATE_INPUT: ${{ github.event.inputs.v5_eos_date }}
|
|
HOTFIX_EXCEPTION_INPUT: ${{ github.event.inputs.hotfix_exception }}
|
|
HOTFIX_REASON_INPUT: ${{ github.event.inputs.hotfix_reason }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
git fetch --prune origin main "${REQUIRED_BRANCH}" --tags
|
|
|
|
RELEASE_NOTES_INPUT="$(jq -r '.inputs.release_notes // ""' "$GITHUB_EVENT_PATH")"
|
|
NOTES_FILE="$(mktemp)"
|
|
printf '%s\n' "$RELEASE_NOTES_INPUT" > "$NOTES_FILE"
|
|
|
|
HELPER_ARGS=(
|
|
--version "${VERSION}"
|
|
--promoted-from-tag "${PROMOTED_FROM_TAG_INPUT:-}"
|
|
--rollback-version "${ROLLBACK_VERSION_INPUT:-}"
|
|
--ga-date "${GA_DATE_INPUT:-}"
|
|
--v5-eos-date "${V5_EOS_DATE_INPUT:-}"
|
|
--hotfix-reason "${HOTFIX_REASON_INPUT:-}"
|
|
--release-notes-file "$NOTES_FILE"
|
|
)
|
|
if [ "${HOTFIX_EXCEPTION_INPUT:-false}" = "true" ]; then
|
|
HELPER_ARGS+=(--hotfix-exception)
|
|
fi
|
|
|
|
python3 scripts/release_control/resolve_release_promotion.py "${HELPER_ARGS[@]}" > "$RUNNER_TEMP/promotion-metadata.out"
|
|
rm -f "$NOTES_FILE"
|
|
|
|
{
|
|
cat "$RUNNER_TEMP/promotion-metadata.out"
|
|
} >> "$GITHUB_OUTPUT"
|
|
|
|
echo "[OK] Promotion policy validated for ${TAG}"
|
|
|
|
# Frontend checks run in parallel with backend tests
|
|
frontend_checks:
|
|
needs: prepare
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 10
|
|
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 dependencies
|
|
run: npm --prefix frontend-modern ci
|
|
|
|
- name: Lint frontend
|
|
run: npm --prefix frontend-modern run lint
|
|
|
|
- name: Audit header composition
|
|
run: npm --prefix frontend-modern run lint:headers
|
|
|
|
- name: Check frontend copy-paste duplication
|
|
run: npm --prefix frontend-modern run lint:cpd
|
|
|
|
# Backend tests run in parallel with frontend checks
|
|
backend_tests:
|
|
needs: prepare
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 30
|
|
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: Restore frontend build cache
|
|
id: frontend-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 (if not cached)
|
|
if: steps.frontend-cache.outputs.cache-hit != 'true'
|
|
run: |
|
|
npm --prefix frontend-modern ci
|
|
npm --prefix frontend-modern run build
|
|
|
|
- name: Copy frontend to embed location
|
|
run: |
|
|
rm -rf internal/api/frontend-modern
|
|
mkdir -p internal/api/frontend-modern
|
|
cp -r frontend-modern/dist internal/api/frontend-modern/
|
|
|
|
- name: Set up Go
|
|
uses: actions/setup-go@v5
|
|
with:
|
|
go-version: '1.25.7'
|
|
cache: true
|
|
|
|
- name: Run backend tests
|
|
env:
|
|
PULSE_DATA_DIR: /tmp/pulse-test-data
|
|
run: make test
|
|
|
|
# Docker build - amd64 only for prereleases, multi-arch for stable
|
|
docker_build:
|
|
needs: prepare
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 30
|
|
permissions:
|
|
contents: read
|
|
packages: write
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Set up QEMU
|
|
if: needs.prepare.outputs.is_prerelease != 'true'
|
|
uses: docker/setup-qemu-action@v3
|
|
|
|
- name: Set up Docker Buildx
|
|
uses: docker/setup-buildx-action@v3
|
|
|
|
- name: Log in to GHCR
|
|
uses: docker/login-action@v3
|
|
with:
|
|
registry: ghcr.io
|
|
username: ${{ github.actor }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Build Docker image (verify only)
|
|
uses: docker/build-push-action@v6
|
|
with:
|
|
context: .
|
|
target: runtime
|
|
# amd64 only for prereleases (faster), multi-arch for stable releases
|
|
platforms: ${{ needs.prepare.outputs.is_prerelease == 'true' && 'linux/amd64' || 'linux/amd64,linux/arm64' }}
|
|
push: false # Don't push staging images, just verify build
|
|
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
|
|
build-args: |
|
|
PULSE_LICENSE_PUBLIC_KEY=${{ secrets.PULSE_LICENSE_PUBLIC_KEY }}
|
|
VERSION=${{ needs.prepare.outputs.tag }}
|
|
|
|
- name: Build Pulse agent image (verify only)
|
|
uses: docker/build-push-action@v6
|
|
with:
|
|
context: .
|
|
file: ./Dockerfile
|
|
target: agent_runtime
|
|
platforms: ${{ needs.prepare.outputs.is_prerelease == 'true' && 'linux/amd64' || 'linux/amd64,linux/arm64' }}
|
|
push: false
|
|
provenance: false
|
|
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/pulse-agent:buildcache
|
|
cache-to: type=registry,ref=ghcr.io/${{ github.repository_owner }}/pulse-agent:buildcache,mode=max
|
|
build-args: |
|
|
PULSE_LICENSE_PUBLIC_KEY=${{ secrets.PULSE_LICENSE_PUBLIC_KEY }}
|
|
VERSION=${{ needs.prepare.outputs.tag }}
|
|
|
|
# Integration tests - skipped for prereleases (they've been tested in CI)
|
|
integration_tests:
|
|
needs:
|
|
- prepare
|
|
- backend_tests
|
|
if: ${{ needs.prepare.outputs.is_prerelease != 'true' }}
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 45
|
|
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: 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: Copy frontend to embed location
|
|
run: |
|
|
rm -rf internal/api/frontend-modern
|
|
mkdir -p internal/api/frontend-modern
|
|
cp -r frontend-modern/dist internal/api/frontend-modern/
|
|
|
|
- name: Set up Go
|
|
uses: actions/setup-go@v5
|
|
with:
|
|
go-version: '1.25.7'
|
|
cache: true
|
|
|
|
- name: Build Pulse Docker image for integration tests
|
|
run: docker build -t pulse:test --target runtime .
|
|
|
|
- name: Build mock GitHub server
|
|
run: docker build -t pulse-mock-github:test tests/integration/mock-github-server
|
|
|
|
- name: Install integration test dependencies
|
|
working-directory: tests/integration
|
|
run: |
|
|
npm ci
|
|
npx playwright install --with-deps chromium
|
|
|
|
- name: Run integration tests
|
|
working-directory: tests/integration
|
|
env:
|
|
MOCK_CHECKSUM_ERROR: "false"
|
|
MOCK_NETWORK_ERROR: "false"
|
|
MOCK_RATE_LIMIT: "false"
|
|
MOCK_STALE_RELEASE: "false"
|
|
PULSE_MULTI_TENANT_ENABLED: "true"
|
|
PULSE_E2E_ENTITLEMENT_PROFILE: "multi-tenant"
|
|
PULSE_E2E_BOOTSTRAP_TOKEN: 0123456789abcdef0123456789abcdef0123456789abcdef
|
|
run: |
|
|
docker compose -f docker-compose.test.yml up -d
|
|
|
|
echo "Waiting for services 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'
|
|
timeout 60 sh -c 'until docker inspect --format="{{json .State.Health.Status}}" pulse-test-server | grep -q "healthy"; do sleep 2; done'
|
|
|
|
for i in 1 2 3 4 5; do
|
|
if curl -f -s http://localhost:7655/api/health > /dev/null 2>&1; then
|
|
echo "Pulse server is reachable"
|
|
break
|
|
elif [ $i -eq 5 ]; then
|
|
docker logs pulse-test-server || true
|
|
exit 1
|
|
fi
|
|
sleep 2
|
|
done
|
|
|
|
node scripts/apply-entitlement-profile.mjs
|
|
|
|
echo "Running update API route smoke check..."
|
|
STATUS=$(curl -s -o /tmp/update-status.json -w "%{http_code}" http://localhost:7655/api/updates/status || true)
|
|
echo "Update status endpoint returned HTTP ${STATUS}"
|
|
case "${STATUS}" in
|
|
200|401|403)
|
|
;;
|
|
*)
|
|
echo "Unexpected response from /api/updates/status"
|
|
cat /tmp/update-status.json || true
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
echo "Running multi-tenant E2E suite..."
|
|
npx playwright test tests/03-multi-tenant.spec.ts --project=chromium --reporter=list
|
|
|
|
docker compose -f docker-compose.test.yml down -v
|
|
|
|
- name: Cleanup
|
|
if: always()
|
|
working-directory: tests/integration
|
|
run: docker compose -f docker-compose.test.yml down -v || true
|
|
|
|
# Create release after all checks pass
|
|
create_release:
|
|
needs:
|
|
- prepare
|
|
- frontend_checks
|
|
- backend_tests
|
|
- docker_build
|
|
- integration_tests
|
|
# Run if integration_tests passed OR was skipped (prereleases)
|
|
if: ${{ always() && needs.frontend_checks.result == 'success' && needs.backend_tests.result == 'success' && needs.docker_build.result == 'success' && (needs.integration_tests.result == 'success' || needs.integration_tests.result == 'skipped') }}
|
|
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
|
|
|
|
- name: Set up Go
|
|
uses: actions/setup-go@v5
|
|
with:
|
|
go-version: '1.25.7'
|
|
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: |
|
|
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.prepare.outputs.tag }}..."
|
|
./scripts/build-release.sh ${{ needs.prepare.outputs.version }}
|
|
env:
|
|
PULSE_LICENSE_PUBLIC_KEY: ${{ secrets.PULSE_LICENSE_PUBLIC_KEY }}
|
|
|
|
- name: Post-build health check
|
|
run: |
|
|
if [ -x ./pulse ]; then
|
|
./pulse --version
|
|
elif [ -x ./cmd/pulse/pulse ]; then
|
|
./cmd/pulse/pulse --version
|
|
fi
|
|
|
|
- name: Prepare release notes
|
|
id: generate_notes
|
|
run: |
|
|
VERSION="${{ needs.prepare.outputs.version }}"
|
|
RELEASE_NOTES_INPUT=$(jq -r '.inputs.release_notes // ""' "$GITHUB_EVENT_PATH" 2>/dev/null || echo "")
|
|
|
|
NOTES_FILE=$(mktemp)
|
|
|
|
if [ -n "$RELEASE_NOTES_INPUT" ]; then
|
|
printf "%s\n" "$RELEASE_NOTES_INPUT" > "$NOTES_FILE"
|
|
else
|
|
echo "Release $VERSION" > "$NOTES_FILE"
|
|
echo "" >> "$NOTES_FILE"
|
|
echo "See commit history for changes." >> "$NOTES_FILE"
|
|
fi
|
|
|
|
{
|
|
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."
|
|
echo ""
|
|
echo "## Promotion Metadata"
|
|
echo ""
|
|
echo "- Promotion channel: ${{ needs.prepare.outputs.is_prerelease == 'true' && 'rc' || 'stable' }}"
|
|
echo "- Candidate stable tag: ${{ needs.prepare.outputs.tag }}"
|
|
if [ -n "${{ needs.prepare.outputs.promoted_from_tag }}" ]; then
|
|
echo "- Promoted prerelease tag: ${{ needs.prepare.outputs.promoted_from_tag }}"
|
|
else
|
|
echo "- Promoted prerelease tag: n/a"
|
|
fi
|
|
echo "- Rollback target: ${{ needs.prepare.outputs.rollback_tag }}"
|
|
echo "- Rollback command: \`${{ needs.prepare.outputs.rollback_command }}\`"
|
|
if [ -n "${{ needs.prepare.outputs.ga_date }}" ]; then
|
|
echo "- Planned GA date: ${{ needs.prepare.outputs.ga_date }}"
|
|
fi
|
|
if [ -n "${{ needs.prepare.outputs.v5_eos_date }}" ]; then
|
|
echo "- Planned v5 end-of-support date: ${{ needs.prepare.outputs.v5_eos_date }}"
|
|
fi
|
|
echo "- Hotfix exception: ${{ needs.prepare.outputs.hotfix_exception }}"
|
|
if [ -n "${{ needs.prepare.outputs.hotfix_reason }}" ]; then
|
|
echo "- Hotfix reason: ${{ needs.prepare.outputs.hotfix_reason }}"
|
|
fi
|
|
} >> "$NOTES_FILE"
|
|
|
|
echo "notes_file=${NOTES_FILE}" >> $GITHUB_OUTPUT
|
|
|
|
- name: Create tag
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
TAG="${{ needs.prepare.outputs.tag }}"
|
|
HEAD_SHA=$(git rev-parse HEAD)
|
|
|
|
REMOTE_TAG_SHA=$(git ls-remote --tags origin "refs/tags/${TAG}" | awk '{print $1}')
|
|
|
|
if [ -n "$REMOTE_TAG_SHA" ]; then
|
|
REMOTE_COMMIT_SHA=$(git ls-remote --tags origin "refs/tags/${TAG}^{}" | awk '{print $1}')
|
|
[ -z "$REMOTE_COMMIT_SHA" ] && REMOTE_COMMIT_SHA="$REMOTE_TAG_SHA"
|
|
|
|
if [ "$REMOTE_COMMIT_SHA" = "$HEAD_SHA" ]; then
|
|
echo "Tag ${TAG} already exists and points to HEAD - continuing"
|
|
else
|
|
echo "::error::Tag ${TAG} already exists but points to ${REMOTE_COMMIT_SHA}, not HEAD (${HEAD_SHA}). Delete the tag first: git push origin --delete ${TAG}"
|
|
exit 1
|
|
fi
|
|
else
|
|
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}"
|
|
fi
|
|
|
|
- name: Create draft release
|
|
id: create_release
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
TAG="${{ needs.prepare.outputs.tag }}"
|
|
NOTES_FILE="${{ steps.generate_notes.outputs.notes_file }}"
|
|
IS_PRERELEASE="${{ needs.prepare.outputs.is_prerelease }}"
|
|
|
|
EXISTING_RELEASE=$(gh api "repos/${{ github.repository }}/releases/tags/${TAG}" 2>/dev/null || echo "")
|
|
RELEASE_ID=$(echo "$EXISTING_RELEASE" | jq -r '.id // empty')
|
|
|
|
if [ -n "$RELEASE_ID" ]; then
|
|
RELEASE_URL=$(echo "$EXISTING_RELEASE" | jq -r '.html_url')
|
|
IS_DRAFT=$(echo "$EXISTING_RELEASE" | jq -r '.draft')
|
|
|
|
if [ "$IS_DRAFT" = "true" ]; then
|
|
echo "Updating existing draft release for ${TAG}"
|
|
gh api "repos/${{ github.repository }}/releases/${RELEASE_ID}" \
|
|
-X PATCH \
|
|
-F body="$(cat $NOTES_FILE)" \
|
|
-F prerelease=${IS_PRERELEASE} > /dev/null
|
|
else
|
|
echo "::error::Published release already exists for ${TAG}."
|
|
exit 1
|
|
fi
|
|
else
|
|
echo "Creating draft release for ${TAG}..."
|
|
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 \
|
|
-F prerelease=${IS_PRERELEASE})
|
|
|
|
RELEASE_ID=$(echo "$RELEASE_JSON" | jq -r '.id')
|
|
RELEASE_URL=$(echo "$RELEASE_JSON" | jq -r '.html_url')
|
|
fi
|
|
|
|
rm -f "$NOTES_FILE"
|
|
|
|
echo "release_url=${RELEASE_URL}" >> $GITHUB_OUTPUT
|
|
echo "release_id=${RELEASE_ID}" >> $GITHUB_OUTPUT
|
|
echo "[OK] Draft release: ${TAG} (ID: ${RELEASE_ID})"
|
|
|
|
- name: Upload checksums
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
TAG="${{ needs.prepare.outputs.tag }}"
|
|
gh release upload "${TAG}" release/checksums.txt --clobber
|
|
gh release upload "${TAG}" release/*.sha256 --clobber
|
|
|
|
- name: Upload release assets
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
TAG="${{ needs.prepare.outputs.tag }}"
|
|
gh release upload "${TAG}" release/*.tar.gz --clobber
|
|
gh release upload "${TAG}" release/*.zip --clobber
|
|
if ls release/*.tgz 1> /dev/null 2>&1; then
|
|
gh release upload "${TAG}" release/*.tgz --clobber
|
|
fi
|
|
for bare_agent in \
|
|
release/pulse-agent-linux-amd64 \
|
|
release/pulse-agent-linux-arm64 \
|
|
release/pulse-agent-linux-armv7 \
|
|
release/pulse-agent-linux-armv6 \
|
|
release/pulse-agent-linux-386 \
|
|
release/pulse-agent-freebsd-amd64 \
|
|
release/pulse-agent-freebsd-arm64 \
|
|
release/pulse-agent-windows-amd64.exe \
|
|
release/pulse-agent-windows-arm64.exe \
|
|
release/pulse-agent-windows-386.exe; do
|
|
if [ -f "${bare_agent}" ]; then
|
|
gh release upload "${TAG}" "${bare_agent}" --clobber
|
|
fi
|
|
done
|
|
gh release upload "${TAG}" release/install.sh --clobber
|
|
if [ -f release/install.ps1 ]; then
|
|
gh release upload "${TAG}" release/install.ps1 --clobber
|
|
fi
|
|
gh release upload "${TAG}" release/install-docker.sh --clobber
|
|
gh release upload "${TAG}" release/pulse-auto-update.sh --clobber
|
|
|
|
- name: Publish release
|
|
if: ${{ github.event.inputs.draft_only != 'true' }}
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
TAG="${{ needs.prepare.outputs.tag }}"
|
|
RELEASE_ID="${{ steps.create_release.outputs.release_id }}"
|
|
IS_PRERELEASE="${{ needs.prepare.outputs.is_prerelease }}"
|
|
|
|
if [ "$IS_PRERELEASE" = "true" ]; then
|
|
gh api "repos/${{ github.repository }}/releases/${RELEASE_ID}" \
|
|
-X PATCH -F draft=false -F make_latest=false
|
|
echo "[OK] Published as prerelease: ${TAG}"
|
|
else
|
|
gh api "repos/${{ github.repository }}/releases/${RELEASE_ID}" \
|
|
-X PATCH -F draft=false -F make_latest=true
|
|
echo "[OK] Published as latest: ${TAG}"
|
|
fi
|
|
|
|
- name: Skip publish (draft only)
|
|
if: ${{ github.event.inputs.draft_only == 'true' }}
|
|
run: 'echo "Draft-only mode: ${{ steps.create_release.outputs.release_url }}"'
|
|
|
|
- name: Trigger Docker image publish
|
|
if: ${{ github.event.inputs.draft_only != 'true' }}
|
|
continue-on-error: true
|
|
env:
|
|
GH_TOKEN: ${{ secrets.WORKFLOW_PAT }}
|
|
run: |
|
|
gh workflow run publish-docker.yml -f tag="${{ needs.prepare.outputs.tag }}"
|
|
echo "[OK] Docker publish workflow dispatched"
|
|
|
|
- name: Trigger demo server update
|
|
if: ${{ github.event.inputs.draft_only != 'true' && needs.prepare.outputs.is_prerelease != 'true' }}
|
|
continue-on-error: true
|
|
env:
|
|
GH_TOKEN: ${{ secrets.WORKFLOW_PAT }}
|
|
run: |
|
|
gh workflow run update-demo-server.yml -f tag="${{ needs.prepare.outputs.tag }}"
|
|
echo "[OK] Demo server update dispatched"
|
|
|
|
- name: Skip demo server update for prerelease
|
|
if: ${{ github.event.inputs.draft_only != 'true' && needs.prepare.outputs.is_prerelease == 'true' }}
|
|
run: echo "Skipping demo server update for prerelease tag ${{ needs.prepare.outputs.tag }}"
|
|
|
|
- name: Summary
|
|
run: |
|
|
echo "[SUCCESS] Release published!"
|
|
echo "Release: ${{ needs.prepare.outputs.tag }}"
|
|
echo "URL: ${{ steps.create_release.outputs.release_url }}"
|
|
|
|
validate_release_assets:
|
|
needs:
|
|
- prepare
|
|
- create_release
|
|
uses: ./.github/workflows/validate-release-assets.yml
|
|
secrets: inherit
|
|
with:
|
|
tag: ${{ needs.prepare.outputs.tag }}
|
|
version: ${{ needs.prepare.outputs.version }}
|
|
release_id: ${{ needs.create_release.outputs.release_id }}
|
|
draft: false
|
|
target_commitish: ${{ needs.create_release.outputs.target_commitish }}
|