Make v5 release automation branch-owned

This commit is contained in:
rcourtman 2026-04-14 19:48:25 +01:00
parent 324f3be1c8
commit 2c51c0a0dd
8 changed files with 158 additions and 180 deletions

View file

@ -4,7 +4,7 @@
**File**: `update-demo-server.yml`
Automatically updates the public demo server when a new stable release is published.
Updates the public demo server when the release pipeline dispatches a new stable release deployment, or when run manually.
### Configuration Required
@ -25,8 +25,8 @@ Add these secrets to your GitHub repository settings (`Settings` → `Secrets an
### How It Works
1. **Trigger**: Runs automatically when a GitHub release is published
2. **Filter**: Only runs for stable releases (skips RC/pre-releases)
1. **Trigger**: Dispatched by the release pipeline after a stable release is published, or run manually from the Actions tab
2. **Filter**: The release pipeline only dispatches this for stable releases
3. **Update**: SSHs to demo server and runs the install script
4. **Verify**: Checks that the new version is running and mock mode is active
5. **Cleanup**: Removes SSH key from runner
@ -49,8 +49,7 @@ To test without publishing a release:
**File**: `helm-ci.yml`
Runs `helm lint --strict` and renders the chart with common configuration combinations on every pull request that touches Helm content (and on pushes to `main`). This prevents regressions before they land.
Runs `helm lint --strict` and renders the chart with common configuration combinations on every pull request that touches Helm content (and on pushes to `main` and `release/5.1`). This prevents regressions before they land.
- Triggered by PRs/pushes touching `deploy/helm/**`, docs, or the workflow itself
- Uses Helm v3.15.2
- Renders both the default deployment and an agent-enabled configuration to catch template issues
@ -59,9 +58,8 @@ Runs `helm lint --strict` and renders the chart with common configuration combin
**File**: `publish-helm-chart.yml`
Packages the Helm chart and pushes it to the GitHub Container Registry (OCI) whenever a GitHub Release is published. Also makes the packaged `.tgz` available as both an Actions artifact and a release asset. The same behaviour can be triggered locally via `./scripts/package-helm-chart.sh <version> [--push]`.
- Triggered automatically on `release: published`, or manually via workflow dispatch (requires `chart_version` input)
Packages the Helm chart and pushes it to the GitHub Container Registry (OCI) when dispatched by the release pipeline, or manually via workflow dispatch. Also makes the packaged `.tgz` available as both an Actions artifact and a release asset. The same behaviour can be triggered locally via `./scripts/package-helm-chart.sh <version> [--push]`.
- Triggered by the release pipeline via workflow dispatch, or manually from the Actions tab
- Chart and app versions mirror the Pulse release tag (e.g., `v4.24.0``4.24.0`)
- Publishes to `oci://ghcr.io/<owner>/pulse-chart`
- Requires no additional secrets—uses the built-in `GITHUB_TOKEN` with `packages: write` permission

View file

@ -503,18 +503,29 @@ jobs:
- 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 \
--ref "${GITHUB_REF_NAME}" \
-f tag="${{ needs.prepare.outputs.tag }}"
-f tag="${{ needs.prepare.outputs.tag }}" \
-f target_branch="${GITHUB_REF_NAME}"
echo "[OK] Docker publish workflow dispatched"
- name: Trigger demo server update
- name: Trigger Helm chart publish
if: ${{ github.event.inputs.draft_only != 'true' }}
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.WORKFLOW_PAT }}
run: |
gh workflow run publish-helm-chart.yml \
--ref "${GITHUB_REF_NAME}" \
-f chart_version="${{ needs.prepare.outputs.version }}" \
-f app_version="${{ needs.prepare.outputs.version }}" \
-f release_tag="${{ needs.prepare.outputs.tag }}"
echo "[OK] Helm chart publish workflow dispatched"
- name: Trigger demo server update
if: ${{ github.event.inputs.draft_only != 'true' && needs.prepare.outputs.is_prerelease != 'true' }}
env:
GH_TOKEN: ${{ secrets.WORKFLOW_PAT }}
run: |

View file

@ -1,17 +1,20 @@
name: Release Helm Chart to GitHub Pages
run-name: Release Helm Chart ${{ inputs.chart_version }}
# Triggered automatically when publish-docker.yml completes, or manually
# We wait for Docker publish because the smoke test pulls the Docker image
# Triggered by publish-docker.yml after image publication, or manually.
# The smoke test pulls the Docker image for the release tag.
on:
workflow_run:
workflows: ["Publish Docker Images"]
types: [completed]
workflow_dispatch:
inputs:
chart_version:
description: "Chart version (e.g., 4.28.0)"
required: true
type: string
target_branch:
description: "Branch to update (defaults to release/5.1)"
required: false
default: "release/5.1"
type: string
permissions:
contents: write
@ -19,13 +22,31 @@ permissions:
jobs:
release:
runs-on: ubuntu-latest
# Only run if workflow_dispatch OR if workflow_run completed successfully
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }}
steps:
- name: Resolve target branch
id: target_branch
run: |
TARGET_BRANCH="${{ inputs.target_branch }}"
if [ -z "$TARGET_BRANCH" ]; then
TARGET_BRANCH="release/5.1"
fi
if [ -z "$TARGET_BRANCH" ]; then
echo "::error::Could not determine target branch"
exit 1
fi
echo "branch=$TARGET_BRANCH" >> "$GITHUB_OUTPUT"
echo "Using target branch: $TARGET_BRANCH"
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ steps.target_branch.outputs.branch }}
- name: Ensure branch checkout
run: git checkout "${{ steps.target_branch.outputs.branch }}"
- name: Configure Git
run: |
@ -46,6 +67,8 @@ jobs:
helm-docs --version
- name: Generate chart documentation
env:
TARGET_BRANCH: ${{ steps.target_branch.outputs.branch }}
run: |
cd deploy/helm/pulse
helm-docs
@ -56,40 +79,26 @@ jobs:
git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
git add README.md
git commit -m "Auto-update Helm chart documentation"
git pull --rebase origin main
git push origin main
git pull --rebase origin "$TARGET_BRANCH"
git push origin HEAD:"$TARGET_BRANCH"
fi
cd ../../..
- name: Determine chart version
- name: Resolve chart version
id: version
env:
GH_TOKEN: ${{ github.token }}
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
# Manual dispatch - use input directly
VERSION="${{ inputs.chart_version }}"
else
# workflow_run trigger - extract version from the triggering workflow
RUN_ID="${{ github.event.workflow_run.id }}"
echo "Extracting version from workflow run ${RUN_ID}..."
WORKFLOW_DATA=$(gh api repos/${{ github.repository }}/actions/runs/${RUN_ID})
TAG=$(echo "$WORKFLOW_DATA" | jq -r '.display_title' | grep -oP 'v?\d+\.\d+\.\d+(-[a-zA-Z]+\.\d+)?' || echo "")
if [ -z "$TAG" ]; then
echo "::error::Could not extract version from workflow_run"
exit 1
fi
# Remove leading 'v' if present
VERSION="${TAG#v}"
VERSION="${{ inputs.chart_version }}"
if [ -z "$VERSION" ]; then
echo "::error::chart_version input is required"
exit 1
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Chart version: $VERSION"
- name: Update Chart.yaml version
env:
TARGET_BRANCH: ${{ steps.target_branch.outputs.branch }}
run: |
VERSION="${{ steps.version.outputs.version }}"
sed -i "s/^version: .*/version: $VERSION/" deploy/helm/pulse/Chart.yaml
@ -101,8 +110,8 @@ jobs:
git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
git add deploy/helm/pulse/Chart.yaml
git commit -m "Auto-update Helm chart version to $VERSION"
git pull --rebase origin main
git push origin main
git pull --rebase origin "$TARGET_BRANCH"
git push origin HEAD:"$TARGET_BRANCH"
fi
- name: Validate Helm chart

View file

@ -1,11 +1,8 @@
name: Promote Floating Tags
run-name: Promote Floating Tags ${{ inputs.tag }}
# Triggered automatically when publish-docker.yml completes, or manually
# Triggered by publish-docker.yml after image publication, or manually.
on:
workflow_run:
workflows: ["Publish Docker Images"]
types: [completed]
workflow_dispatch:
inputs:
tag:
@ -21,52 +18,22 @@ on:
jobs:
promote-images:
runs-on: ubuntu-latest
# Only run if workflow_dispatch OR if workflow_run completed successfully
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }}
permissions:
contents: read
packages: write
steps:
- name: Extract tag from trigger
- name: Resolve release tag
id: extract
env:
GH_TOKEN: ${{ github.token }}
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
# Manual dispatch - use inputs directly
TAG="${{ inputs.tag }}"
PRERELEASE="${{ inputs.prerelease }}"
else
# workflow_run trigger - extract from the triggering workflow's inputs
# The publish-docker workflow was triggered with a tag input
RUN_ID="${{ github.event.workflow_run.id }}"
echo "Extracting inputs from workflow run ${RUN_ID}..."
# Get the workflow run details to extract the tag
WORKFLOW_DATA=$(gh api repos/${{ github.repository }}/actions/runs/${RUN_ID})
TAG=$(echo "$WORKFLOW_DATA" | jq -r '.head_branch // ""')
# If head_branch is main, we need to get it from the run's inputs
# The inputs are stored in the run's display_title or we parse from artifacts
if [ "$TAG" = "main" ] || [ -z "$TAG" ]; then
# Try to get from run name which typically includes the tag
TAG=$(echo "$WORKFLOW_DATA" | jq -r '.display_title' | grep -oP 'v\d+\.\d+\.\d+(-[a-zA-Z]+\.\d+)?' || echo "")
fi
if [ -z "$TAG" ]; then
echo "::error::Could not extract tag from workflow_run"
exit 1
fi
# Detect prerelease from tag
if [[ "$TAG" =~ -rc\.[0-9]+$ ]] || [[ "$TAG" =~ -alpha\.[0-9]+$ ]] || [[ "$TAG" =~ -beta\.[0-9]+$ ]]; then
PRERELEASE="true"
else
PRERELEASE="false"
fi
TAG="${{ inputs.tag }}"
PRERELEASE="${{ inputs.prerelease }}"
if [ -z "$TAG" ]; then
echo "::error::tag input is required"
exit 1
fi
echo "tag=${TAG}" >> $GITHUB_OUTPUT
echo "prerelease=${PRERELEASE}" >> $GITHUB_OUTPUT
echo "Promoting floating tags for ${TAG} (prerelease: ${PRERELEASE})"
@ -92,7 +59,7 @@ jobs:
MAX_ATTEMPTS=30
ATTEMPT=0
# Wait for main pulse image
# Wait for Pulse server image
while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do
if docker manifest inspect rcourtman/pulse:${TAG} > /dev/null 2>&1; then
echo "Image rcourtman/pulse:${TAG} is available!"

View file

@ -2,7 +2,8 @@ name: Publish Docker Images
run-name: Publish Docker Images ${{ inputs.tag }}
# Triggered by create-release.yml after staging images pass tests.
# Builds multi-arch images (amd64+arm64) from source and publishes to Docker Hub and GHCR.
# Builds multi-arch images (amd64+arm64) from source, publishes to Docker Hub and GHCR,
# then dispatches the downstream maintenance-branch workflows that depend on those images.
on:
workflow_dispatch:
inputs:
@ -10,6 +11,10 @@ on:
description: 'Release tag (e.g., v4.34.0)'
required: true
type: string
target_branch:
description: 'Branch to dispatch downstream workflows on (defaults to the workflow ref)'
required: false
type: string
concurrency:
group: docker-publish-${{ inputs.tag }}
@ -47,6 +52,22 @@ jobs:
echo "is_prerelease=${IS_PRERELEASE}" >> $GITHUB_OUTPUT
echo "Publishing Docker images for ${TAG} (prerelease: ${IS_PRERELEASE})"
- name: Resolve downstream branch
id: target_branch
run: |
TARGET_BRANCH="${{ inputs.target_branch }}"
if [ -z "$TARGET_BRANCH" ]; then
TARGET_BRANCH="${GITHUB_REF_NAME}"
fi
if [ -z "$TARGET_BRANCH" ]; then
echo "::error::Could not determine downstream target branch"
exit 1
fi
echo "branch=${TARGET_BRANCH}" >> "$GITHUB_OUTPUT"
echo "Using downstream target branch: ${TARGET_BRANCH}"
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
@ -129,3 +150,23 @@ jobs:
if [ "$IS_PRERELEASE" = "true" ]; then
echo "Note: :latest tags were NOT updated (this is a prerelease)"
fi
- name: Trigger floating tag promotion
env:
GH_TOKEN: ${{ secrets.WORKFLOW_PAT }}
run: |
gh workflow run promote-floating-tags.yml \
--ref "${{ steps.target_branch.outputs.branch }}" \
-f tag="${{ steps.version.outputs.tag }}" \
-f prerelease="${{ steps.version.outputs.is_prerelease }}"
echo "[OK] Floating tag promotion workflow dispatched"
- name: Trigger Helm Pages release
env:
GH_TOKEN: ${{ secrets.WORKFLOW_PAT }}
run: |
gh workflow run helm-pages.yml \
--ref "${{ steps.target_branch.outputs.branch }}" \
-f chart_version="${{ steps.version.outputs.version }}" \
-f target_branch="${{ steps.target_branch.outputs.branch }}"
echo "[OK] Helm Pages workflow dispatched"

View file

@ -1,16 +1,20 @@
name: Publish Helm Chart
on:
release:
types: [published]
workflow_dispatch:
inputs:
chart_version:
description: "Chart version (required when running manually, use format 4.24.0)"
required: true
type: string
app_version:
description: "Application version to embed (defaults to chart version)"
required: false
type: string
release_tag:
description: "Release tag to attach the chart asset to (e.g., v5.1.28)"
required: true
type: string
jobs:
publish:
@ -31,27 +35,23 @@ jobs:
- name: Determine chart version
id: versions
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
CHART_VERSION="${{ inputs.chart_version }}"
if [ -z "$CHART_VERSION" ]; then
echo "::error::chart_version input is required when running manually"
exit 1
fi
APP_VERSION="${{ inputs.app_version }}"
if [ -z "$APP_VERSION" ]; then
APP_VERSION="$CHART_VERSION"
fi
RELEASE_TAG="$CHART_VERSION"
else
RELEASE_TAG="${{ github.event.release.tag_name }}"
if [ -z "$RELEASE_TAG" ]; then
echo "::error::Release tag is empty"
exit 1
fi
CHART_VERSION="${RELEASE_TAG#v}"
CHART_VERSION="${{ inputs.chart_version }}"
if [ -z "$CHART_VERSION" ]; then
echo "::error::chart_version input is required"
exit 1
fi
APP_VERSION="${{ inputs.app_version }}"
if [ -z "$APP_VERSION" ]; then
APP_VERSION="$CHART_VERSION"
fi
RELEASE_TAG="${{ inputs.release_tag }}"
if [ -z "$RELEASE_TAG" ]; then
echo "::error::release_tag input is required"
exit 1
fi
echo "chart_version=$CHART_VERSION" >> "$GITHUB_OUTPUT"
echo "app_version=$APP_VERSION" >> "$GITHUB_OUTPUT"
echo "release_tag=$RELEASE_TAG" >> "$GITHUB_OUTPUT"
@ -97,7 +97,6 @@ jobs:
echo "Package visibility configuration attempted. Verify at: https://github.com/${{ github.repository_owner }}?tab=packages"
- name: Attach chart to release
if: github.event_name == 'release'
env:
GITHUB_TOKEN: ${{ github.token }}
run: |

View file

@ -1,8 +1,6 @@
name: Update Demo Server
on:
release:
types: [published]
workflow_dispatch:
inputs:
tag:
@ -15,51 +13,24 @@ permissions:
jobs:
update-demo:
# Only run for stable releases (not pre-releases) or manual dispatch
if: github.event_name == 'workflow_dispatch' || github.event.release.prerelease == false
runs-on: ubuntu-latest
steps:
- name: Resolve target tag
id: target
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
TAG="${{ inputs.tag }}"
else
TAG="${{ github.event.release.tag_name }}"
TAG="${{ inputs.tag }}"
if [ -z "$TAG" ]; then
echo "::error::tag input is required"
exit 1
fi
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
- name: Skip if not latest published release
id: gate
if: github.event_name == 'release'
env:
GH_TOKEN: ${{ github.token }}
- name: Confirm deployment target
run: |
TARGET="${{ steps.target.outputs.tag }}"
LATEST=$(gh api "repos/${{ github.repository }}/releases?per_page=100" --jq 'map(select(.draft == false and .prerelease == false and (.tag_name | test("^v5\\.1\\.[0-9]+$"))))[0].tag_name')
echo "Target tag: $TARGET"
echo "Latest published v5.1 tag: $LATEST"
if [ "$TARGET" != "$LATEST" ]; then
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "Release is not the latest v5.1 stable release; skipping demo update."
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
- name: Check release type
if: steps.gate.outputs.skip != 'true'
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "Manual deployment triggered for: ${{ inputs.tag }}"
else
echo "Release: ${{ github.event.release.tag_name }}"
echo "Prerelease: ${{ github.event.release.prerelease }}"
echo "Updating demo server to latest stable v5.1 release..."
fi
echo "Deploying demo server for tag: ${{ steps.target.outputs.tag }}"
- name: Wait for release assets
if: steps.gate.outputs.skip != 'true'
run: |
TAG="${{ steps.target.outputs.tag }}"
echo "Waiting for release assets to be available..."
@ -94,7 +65,6 @@ jobs:
exit 1
- name: Setup SSH
if: steps.gate.outputs.skip != 'true'
run: |
mkdir -p ~/.ssh
echo "${{ secrets.DEMO_SERVER_SSH_KEY }}" > ~/.ssh/id_ed25519
@ -102,7 +72,6 @@ jobs:
ssh-keyscan -H ${{ secrets.DEMO_SERVER_HOST }} >> ~/.ssh/known_hosts
- name: Check current demo version
if: steps.gate.outputs.skip != 'true'
id: current
run: |
TARGET="${{ steps.target.outputs.tag }}"
@ -118,7 +87,7 @@ jobs:
fi
- name: Update demo server
if: steps.gate.outputs.skip != 'true' && steps.current.outputs.skip_current != 'true'
if: steps.current.outputs.skip_current != 'true'
run: |
TAG="${{ steps.target.outputs.tag }}"
# Use set -o pipefail to ensure curl errors aren't masked by bash
@ -126,7 +95,7 @@ jobs:
"set -o pipefail && curl -fsSL https://raw.githubusercontent.com/rcourtman/Pulse/release/5.1/install.sh | sudo bash -s -- --version $TAG"
- name: Verify update
if: steps.gate.outputs.skip != 'true' && steps.current.outputs.skip_current != 'true'
if: steps.current.outputs.skip_current != 'true'
run: |
# Wait a moment for service to restart
sleep 5
@ -160,5 +129,5 @@ jobs:
fi
- name: Cleanup SSH key
if: always() && steps.gate.outputs.skip != 'true'
if: always()
run: rm -f ~/.ssh/id_ed25519

View file

@ -23,8 +23,6 @@ on:
description: 'Commit SHA associated with the release'
required: true
type: string
release:
types: [edited]
workflow_dispatch:
inputs:
tag:
@ -61,37 +59,17 @@ jobs:
- name: Determine release context
id: context
env:
EVENT_NAME: ${{ github.event_name }}
INPUT_TAG: ${{ inputs.tag }}
INPUT_VERSION: ${{ inputs.version }}
INPUT_RELEASE_ID: ${{ inputs.release_id }}
INPUT_DRAFT: ${{ inputs.draft }}
INPUT_COMMIT: ${{ inputs.target_commitish }}
run: |
python3 <<'EOF' > context.env
import json, os, sys
import os, sys
event_name = os.environ.get("EVENT_NAME", "")
result = {}
if event_name == "release":
with open(os.environ["GITHUB_EVENT_PATH"], "r", encoding="utf-8") as handle:
data = json.load(handle)
release = data.get("release") or {}
result["tag"] = release.get("tag_name", "")
tag = result["tag"]
result["version"] = tag[1:] if tag.startswith("v") else tag
result["release_id"] = str(release.get("id", ""))
result["target_commitish"] = release.get("target_commitish", "")
result["draft"] = str(release.get("draft", False)).lower()
else:
result["tag"] = os.environ.get("INPUT_TAG", "")
result["version"] = os.environ.get("INPUT_VERSION", "")
result["release_id"] = os.environ.get("INPUT_RELEASE_ID", "")
result["target_commitish"] = os.environ.get("INPUT_COMMIT", "")
draft_value = os.environ.get("INPUT_DRAFT", "false")
result["draft"] = str(draft_value).lower()
result = {
"tag": os.environ.get("INPUT_TAG", ""),
"version": os.environ.get("INPUT_VERSION", ""),
"release_id": os.environ.get("INPUT_RELEASE_ID", ""),
"target_commitish": os.environ.get("INPUT_COMMIT", ""),
"draft": str(os.environ.get("INPUT_DRAFT", "false")).lower(),
}
if not result["tag"] or not result["release_id"]:
sys.stderr.write("::error::Release metadata is missing. Provide tag, version, release_id, and target_commitish.\n")
@ -104,6 +82,12 @@ jobs:
EOF
cat context.env >> "$GITHUB_OUTPUT"
cat context.env
env:
INPUT_TAG: ${{ inputs.tag }}
INPUT_VERSION: ${{ inputs.version }}
INPUT_RELEASE_ID: ${{ inputs.release_id }}
INPUT_DRAFT: ${{ inputs.draft }}
INPUT_COMMIT: ${{ inputs.target_commitish }}
- name: Download all release assets
if: steps.context.outputs.should_run == 'true'