fix(release): stage artifacts before publication

This commit is contained in:
Richard Courtman
2026-08-07 12:27:36 +01:00
parent d699dc66a0
commit 5e9db16927
9 changed files with 753 additions and 350 deletions
+269 -76
View File
@@ -1170,44 +1170,13 @@ jobs:
release_upload_with_retry "${TAG}" release/install-docker.sh --clobber
release_upload_with_retry "${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
# 'latest' belongs to the highest stable semver overall. A
# maintenance cut of an older line (e.g. v5.1.36 after v6 GA)
# publishes without stealing the latest marker from the current
# line.
HIGHEST_STABLE=$(gh api --paginate "repos/${{ github.repository }}/tags" --jq '.[].name' \
| grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -1)
if [ "$TAG" = "$HIGHEST_STABLE" ]; then
gh api "repos/${{ github.repository }}/releases/${RELEASE_ID}" \
-X PATCH -F draft=false -F make_latest=true
echo "[OK] Published as latest: ${TAG}"
else
gh api "repos/${{ github.repository }}/releases/${RELEASE_ID}" \
-X PATCH -F draft=false -F make_latest=false
echo "[OK] Published WITHOUT latest marker: ${TAG} (highest stable is ${HIGHEST_STABLE})"
fi
fi
- name: Skip publish (draft only)
- name: Stop after staging (draft only)
if: ${{ github.event.inputs.draft_only == 'true' }}
run: 'echo "Draft-only mode: ${{ steps.create_release.outputs.release_url }}"'
- name: Summary
run: |
echo "[SUCCESS] Release published!"
echo "[SUCCESS] Release assets staged behind an unpublished draft."
echo "Release: ${{ needs.prepare.outputs.tag }}"
echo "URL: ${{ steps.create_release.outputs.release_url }}"
@@ -1295,30 +1264,30 @@ jobs:
tag: ${{ needs.prepare.outputs.tag }}
version: ${{ needs.prepare.outputs.version }}
release_id: ${{ needs.create_release.outputs.release_id }}
draft: ${{ github.event.inputs.draft_only == 'true' }}
draft: true
target_commitish: ${{ needs.create_release.outputs.target_commitish }}
candidate_manifest_artifact: ${{ needs.build_release_candidate.outputs.manifest_artifact_name }}
# End-to-end install.sh smoke against the just-published release. Catches
# End-to-end install.sh smoke against the staged draft release. Catches
# runtime regressions in the documented Proxmox-LXC / systemd install flow
# that the build-time validate-release.sh checks cannot see: the script
# parses fine, signs cleanly, but fails to actually install or boot Pulse.
# This class of regression broke silently across v6 rc.1 → rc.5 because no
# existing gate exercised the documented secure-install commands against
# the published GitHub Release URL.
# the exact GitHub Release asset bytes before the customer notification.
#
# Gated on validate_release_assets success — the smoke depends on the
# published asset bundle being well-formed, so we only run it after the
# staged asset bundle being well-formed, so we only run it after the
# cheaper content checks pass. Skipped for the historical-backfill path
# since that flow re-uploads to an already-published release and the
# smoke would just re-confirm what hasn't changed. Also skipped for
# draft-only runs because draft release assets are not available at the
# public /releases/download/<tag>/ URL this smoke intentionally exercises.
# smoke would just re-confirm what hasn't changed. Draft-only runs stop after
# validation and do not enter the customer activation sequence.
install_sh_smoke:
needs:
- prepare
- create_release
- validate_release_assets
if: ${{ always() && needs.prepare.result == 'success' && needs.validate_release_assets.result == 'success' && needs.prepare.outputs.historical_asset_backfill_only != 'true' && github.event.inputs.draft_only != 'true' }}
if: ${{ always() && needs.prepare.result == 'success' && needs.create_release.result == 'success' && needs.validate_release_assets.result == 'success' && needs.prepare.outputs.historical_asset_backfill_only != 'true' && github.event.inputs.draft_only != 'true' }}
permissions:
contents: read
uses: ./.github/workflows/install-sh-smoke.yml
@@ -1327,12 +1296,15 @@ jobs:
tag: ${{ needs.prepare.outputs.tag }}
version: ${{ needs.prepare.outputs.version }}
repository: ${{ github.repository }}
asset_source: staged
release_id: ${{ needs.create_release.outputs.release_id }}
update_stable_demo:
needs:
- prepare
- validate_release_assets
if: ${{ always() && needs.prepare.result == 'success' && needs.validate_release_assets.result == 'success' && needs.prepare.outputs.historical_asset_backfill_only != 'true' && github.event.inputs.draft_only != 'true' && needs.prepare.outputs.is_prerelease != 'true' && startsWith(needs.prepare.outputs.version, '6.') }}
- create_release
- release_readiness
if: ${{ always() && needs.prepare.result == 'success' && needs.create_release.result == 'success' && needs.release_readiness.result == 'success' && needs.prepare.outputs.is_prerelease != 'true' && startsWith(needs.prepare.outputs.version, '6.') }}
permissions:
contents: read
uses: ./.github/workflows/update-demo-server.yml
@@ -1341,6 +1313,7 @@ jobs:
tag: ${{ needs.prepare.outputs.tag }}
target: stable
verify_only: false
release_id: ${{ needs.create_release.outputs.release_id }}
# Publish the Helm chart for this release. publish-helm-chart.yml also
# listens for `release: published` events directly, but the create_release
@@ -1366,20 +1339,52 @@ jobs:
chart_version: ${{ needs.prepare.outputs.version }}
app_version: ${{ needs.prepare.outputs.version }}
# Defensive backup to promote-floating-tags.yml's workflow_run chain off
# publish-docker.yml. The chain works when publish-docker succeeds, but
# when it fails the floating tags don't advance and customers pulling
# rcourtman/pulse:latest stay on whatever the previous successful release
# tagged. Calling promote-floating-tags as workflow_call after
# validate_release_assets and publish_docker succeed guarantees the floating
# tags advance. Draft-only runs must not promote floating tags because the
# release is still in private promotion state.
promote_floating_tags:
# Stage and prove the customer-facing Helm Pages repository explicitly.
# The legacy workflow_run chain was asynchronous and could leave the public
# index behind an already-visible GitHub release.
publish_helm_pages:
needs:
- prepare
- publish_docker
- validate_release_assets
if: ${{ always() && needs.prepare.result == 'success' && needs.publish_docker.result == 'success' && needs.validate_release_assets.result == 'success' && needs.prepare.outputs.historical_asset_backfill_only != 'true' && github.event.inputs.draft_only != 'true' }}
- publish_helm_chart
if: ${{ always() && needs.prepare.result == 'success' && needs.publish_docker.result == 'success' && needs.validate_release_assets.result == 'success' && needs.publish_helm_chart.result == 'success' && needs.prepare.outputs.historical_asset_backfill_only != 'true' && github.event.inputs.draft_only != 'true' }}
permissions:
contents: write
uses: ./.github/workflows/helm-pages.yml
secrets: inherit
with:
chart_version: ${{ needs.prepare.outputs.version }}
# One immutable-readiness gate joins every exact-version path before any
# mutable customer pointer moves. v6 additionally requires the staged Pro
# image and signed packet; older release lines have no private Pro job.
release_readiness:
needs:
- prepare
- create_release
- publish_docker
- validate_release_assets
- install_sh_smoke
- publish_helm_chart
- publish_helm_pages
- stage_private_pro_runtime
if: ${{ always() && needs.prepare.result == 'success' && needs.create_release.result == 'success' && needs.publish_docker.result == 'success' && needs.validate_release_assets.result == 'success' && needs.install_sh_smoke.result == 'success' && needs.publish_helm_chart.result == 'success' && needs.publish_helm_pages.result == 'success' && ( !startsWith(needs.prepare.outputs.version, '6.') || needs.stage_private_pro_runtime.result == 'success' ) && needs.prepare.outputs.historical_asset_backfill_only != 'true' && github.event.inputs.draft_only != 'true' }}
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- name: Confirm immutable release readiness
run: echo "All exact-version release paths are ready for customer activation."
# Mutable image aliases move only after the exact-version images and staged
# release assets have passed validation. promote-floating-tags.yml has no
# implicit workflow_run trigger, so this explicit call is the sole owner of
# rc/latest/major/minor pointers for both runtime images.
promote_floating_tags:
needs:
- prepare
- release_readiness
if: ${{ always() && needs.prepare.result == 'success' && needs.release_readiness.result == 'success' }}
permissions:
contents: read
packages: write
@@ -1389,26 +1394,21 @@ jobs:
tag: ${{ needs.prepare.outputs.tag }}
prerelease: ${{ needs.prepare.outputs.is_prerelease == 'true' }}
# Customer-facing v6 public releases must not outrun the private Pulse Pro
# runtime path. The public release is the immutable source tag; this job
# dispatches the private build against that exact tag, waits for the R2 and
# Docker publication workflow to pass, then dispatches the pulse-pro live
# promotion workflow and waits for the signed packet to update the license
# broker. A failure here fails the release pipeline instead of leaving paid
# customers on a stale private manifest. Rerunning this job after a
# promotion-only failure is safe: the R2 prefix is derived from run-stable
# values and the build is dispatched with reuse_existing_packet=true, so the
# enterprise side validates the packet the earlier attempt uploaded, skips
# the rebuild, and only the promotion re-executes.
publish_private_pro_runtime:
# Stage the exact private Pro image and signed R2 packet as soon as the draft
# release/tag exist. This slow build runs in parallel with public artifact
# validation, but does not update the live paid-runtime broker manifest.
stage_private_pro_runtime:
needs:
- prepare
- create_release
if: ${{ always() && needs.prepare.result == 'success' && needs.create_release.result == 'success' && needs.prepare.outputs.historical_asset_backfill_only != 'true' && github.event.inputs.draft_only != 'true' && startsWith(needs.prepare.outputs.version, '6.') }}
runs-on: ubuntu-24.04
timeout-minutes: 150
timeout-minutes: 120
outputs:
r2_prefix: ${{ steps.publish.outputs.r2_prefix }}
steps:
- name: Dispatch and verify private Pro runtime publication
- name: Dispatch and verify private Pro runtime staging
id: publish
env:
GH_TOKEN: ${{ secrets.WORKFLOW_PAT }}
VERSION: ${{ needs.prepare.outputs.version }}
@@ -1500,6 +1500,7 @@ jobs:
exit 1
fi
r2_prefix="${TAG}-pro-${run_created_date}-${GITHUB_RUN_ID}"
echo "r2_prefix=${r2_prefix}" >> "$GITHUB_OUTPUT"
build_started_at="$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
echo "Dispatching private Pro build for ${TAG} with R2 prefix ${r2_prefix}."
gh workflow run build-pro-release.yml \
@@ -1516,16 +1517,196 @@ jobs:
-f allow_stable_ga_publish="${allow_ga_publish}"
wait_for_workflow rcourtman/pulse-enterprise "Build Pro Release" main "${build_started_at}" "private Pro build" 7200
# The paid-runtime broker is a mutable customer pointer, so promote it only
# after every exact-version public artifact and staged install path is ready.
# A rerun reuses the run-stable R2 prefix produced by the staging job.
promote_private_pro_runtime:
needs:
- prepare
- release_readiness
- stage_private_pro_runtime
if: ${{ always() && needs.prepare.result == 'success' && needs.release_readiness.result == 'success' && needs.stage_private_pro_runtime.result == 'success' && startsWith(needs.prepare.outputs.version, '6.') }}
runs-on: ubuntu-24.04
timeout-minutes: 65
steps:
- name: Promote and verify private Pro runtime
env:
GH_TOKEN: ${{ secrets.WORKFLOW_PAT }}
VERSION: ${{ needs.prepare.outputs.version }}
TAG: ${{ needs.prepare.outputs.tag }}
IS_PRERELEASE: ${{ needs.prepare.outputs.is_prerelease }}
R2_PREFIX: ${{ needs.stage_private_pro_runtime.outputs.r2_prefix }}
run: |
set -euo pipefail
if [[ -z "${GH_TOKEN:-}" ]]; then
echo "::error::WORKFLOW_PAT is required to dispatch private Pro promotion workflows."
exit 1
fi
if [[ -z "${R2_PREFIX:-}" ]]; then
echo "::error::Private Pro staging did not return an R2 prefix."
exit 1
fi
wait_for_workflow() {
local repo="$1"
local workflow="$2"
local branch="$3"
local started_at="$4"
local label="$5"
local timeout_seconds="$6"
local deadline=$((SECONDS + timeout_seconds))
local run_id=""
while (( SECONDS < deadline )); do
if [[ -z "${run_id}" ]]; then
run_id="$(
gh run list \
--repo "${repo}" \
--workflow "${workflow}" \
--event workflow_dispatch \
--branch "${branch}" \
--limit 50 \
--json databaseId,createdAt \
--jq "map(select(.createdAt >= \"${started_at}\")) | sort_by(.createdAt) | reverse | .[0].databaseId // \"\""
)"
if [[ -n "${run_id}" ]]; then
echo "Watching ${label} run ${run_id} in ${repo}."
else
echo "Waiting for ${label} workflow run to appear..."
fi
fi
if [[ -n "${run_id}" ]]; then
run_state="$(
gh run view "${run_id}" \
--repo "${repo}" \
--json status,conclusion,url \
--jq '[.status, (.conclusion // ""), .url] | @tsv'
)"
status="$(awk -F '\t' '{print $1}' <<<"${run_state}")"
conclusion="$(awk -F '\t' '{print $2}' <<<"${run_state}")"
url="$(awk -F '\t' '{print $3}' <<<"${run_state}")"
echo "${label}: status=${status} conclusion=${conclusion:-pending} ${url}"
if [[ "${status}" == "completed" ]]; then
if [[ "${conclusion}" == "success" ]]; then
echo "[OK] ${label} completed successfully: ${url}"
return 0
fi
echo "::error::${label} failed with conclusion=${conclusion}: ${url}"
return 1
fi
fi
sleep 30
done
echo "::error::Timed out waiting for ${label} after ${timeout_seconds}s."
return 1
}
allow_ga_publish=false
if [[ "${IS_PRERELEASE}" != "true" ]]; then
allow_ga_publish=true
fi
promote_started_at="$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
echo "Dispatching live paid-runtime promotion for ${TAG} with R2 prefix ${r2_prefix}."
echo "Dispatching live paid-runtime promotion for ${TAG} with R2 prefix ${R2_PREFIX}."
gh workflow run promote-paid-runtime-release.yml \
--repo rcourtman/pulse-pro \
--ref main \
-f version="${VERSION}" \
-f r2_prefix="${r2_prefix}" \
-f r2_prefix="${R2_PREFIX}" \
-f allow_ga_prefix="${allow_ga_publish}"
wait_for_workflow rcourtman/pulse-pro "Promote Paid Runtime Release" main "${promote_started_at}" "private Pro live promotion" 3600
# Customer activation is one final publication barrier. All exact-version
# artifacts, mutable image aliases, and the live paid-runtime manifest are
# ready before the GitHub release becomes visible. If public asset reads fail
# immediately after activation, quarantine the release back to draft.
activate_release:
needs:
- prepare
- create_release
- release_readiness
- update_stable_demo
- promote_floating_tags
- promote_private_pro_runtime
if: ${{ always() && needs.prepare.result == 'success' && needs.create_release.result == 'success' && needs.release_readiness.result == 'success' && needs.promote_floating_tags.result == 'success' && ( !startsWith(needs.prepare.outputs.version, '6.') || needs.promote_private_pro_runtime.result == 'success' ) && ( !startsWith(needs.prepare.outputs.version, '6.') || needs.prepare.outputs.is_prerelease == 'true' || needs.update_stable_demo.result == 'success' ) }}
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: write
steps:
- name: Publish the fully staged release
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ needs.prepare.outputs.tag }}
RELEASE_ID: ${{ needs.create_release.outputs.release_id }}
EXPECTED_COMMIT: ${{ needs.create_release.outputs.target_commitish }}
IS_PRERELEASE: ${{ needs.prepare.outputs.is_prerelease }}
run: |
set -euo pipefail
release_json=$(mktemp)
publish_payload=$(mktemp)
quarantine_payload=$(mktemp)
activated=false
quarantine_on_error() {
if [ "$activated" = "true" ]; then
echo "::warning::Public asset verification failed; returning ${TAG} to draft quarantine."
gh api "repos/${{ github.repository }}/releases/${RELEASE_ID}" \
-X PATCH --input "$quarantine_payload" >/dev/null || true
fi
}
trap quarantine_on_error ERR
gh api "repos/${{ github.repository }}/releases/${RELEASE_ID}" > "$release_json"
actual_tag=$(jq -r '.tag_name // ""' "$release_json")
actual_commit=$(jq -r '.target_commitish // ""' "$release_json")
actual_draft=$(jq -r '.draft' "$release_json")
published_at=$(jq -r '.published_at // ""' "$release_json")
actual_prerelease=$(jq -r '.prerelease' "$release_json")
if [ "$actual_tag" != "$TAG" ] || [ "$actual_commit" != "$EXPECTED_COMMIT" ] || \
[ "$actual_draft" != "true" ] || [ -n "$published_at" ] || \
[ "$actual_prerelease" != "$IS_PRERELEASE" ]; then
echo "::error::Release ${RELEASE_ID} no longer matches the staged activation candidate."
exit 1
fi
make_latest=false
if [ "$IS_PRERELEASE" != "true" ]; then
highest_stable=$(gh api --paginate "repos/${{ github.repository }}/tags" --jq '.[].name' \
| grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -1)
if [ "$TAG" = "$highest_stable" ]; then
make_latest=true
fi
fi
jq -n --arg make_latest "$make_latest" \
'{draft: false, make_latest: $make_latest}' > "$publish_payload"
jq -n '{draft: true, make_latest: "false"}' > "$quarantine_payload"
gh api "repos/${{ github.repository }}/releases/${RELEASE_ID}" \
-X PATCH --input "$publish_payload" > "$release_json"
activated=true
if [ "$(jq -r '.draft' "$release_json")" != "false" ] || \
[ -z "$(jq -r '.published_at // ""' "$release_json")" ]; then
echo "::error::GitHub did not publish release ${RELEASE_ID}."
exit 1
fi
base="https://github.com/${{ github.repository }}/releases/download/${TAG}"
for asset_name in \
checksums.txt \
install.sh \
"pulse-${TAG}-linux-amd64.tar.gz"; do
curl -fsSL --retry 12 --retry-delay 5 --retry-all-errors \
-o /dev/null "${base}/${asset_name}"
done
trap - ERR
rm -f "$release_json" "$publish_payload" "$quarantine_payload"
echo "[OK] Activated ${TAG} after every customer-facing release path was ready."
release_verdict:
name: Definitive Release Verdict
needs:
@@ -1538,8 +1719,12 @@ jobs:
- install_sh_smoke
- update_stable_demo
- publish_helm_chart
- publish_helm_pages
- release_readiness
- promote_floating_tags
- publish_private_pro_runtime
- stage_private_pro_runtime
- promote_private_pro_runtime
- activate_release
if: ${{ always() && needs.prepare.result == 'success' && needs.prepare.outputs.historical_asset_backfill_only != 'true' }}
runs-on: ubuntu-24.04
steps:
@@ -1556,8 +1741,12 @@ jobs:
INSTALL_RESULT: ${{ needs.install_sh_smoke.result }}
DEMO_RESULT: ${{ needs.update_stable_demo.result }}
HELM_RESULT: ${{ needs.publish_helm_chart.result }}
HELM_PAGES_RESULT: ${{ needs.publish_helm_pages.result }}
READINESS_RESULT: ${{ needs.release_readiness.result }}
FLOATING_RESULT: ${{ needs.promote_floating_tags.result }}
PRIVATE_PRO_RESULT: ${{ needs.publish_private_pro_runtime.result }}
PRIVATE_PRO_STAGE_RESULT: ${{ needs.stage_private_pro_runtime.result }}
PRIVATE_PRO_PROMOTION_RESULT: ${{ needs.promote_private_pro_runtime.result }}
ACTIVATION_RESULT: ${{ needs.activate_release.result }}
run: |
set -euo pipefail
require_result() {
@@ -1572,16 +1761,20 @@ jobs:
require_result "release smoke" "$SMOKE_RESULT" success
require_result "Windows install command smoke" "$WINDOWS_INSTALL_COMMAND_RESULT" success
require_result "release assembly" "$CREATE_RESULT" success
require_result "release staging" "$CREATE_RESULT" success
require_result "release asset validation" "$VALIDATE_RESULT" success
if [ "${DRAFT_ONLY:-false}" != "true" ]; then
require_result "Docker publication" "$DOCKER_RESULT" success
require_result "install.sh smoke" "$INSTALL_RESULT" success
require_result "Helm publication" "$HELM_RESULT" success
require_result "exact-version Docker staging" "$DOCKER_RESULT" success
require_result "staged install.sh smoke" "$INSTALL_RESULT" success
require_result "Helm staging" "$HELM_RESULT" success
require_result "public Helm repository staging" "$HELM_PAGES_RESULT" success
require_result "immutable release readiness" "$READINESS_RESULT" success
require_result "floating-tag promotion" "$FLOATING_RESULT" success
require_result "release activation" "$ACTIVATION_RESULT" success
if [[ "$VERSION" == 6.* ]]; then
require_result "private Pro publication" "$PRIVATE_PRO_RESULT" success
require_result "private Pro staging" "$PRIVATE_PRO_STAGE_RESULT" success
require_result "private Pro live promotion" "$PRIVATE_PRO_PROMOTION_RESULT" success
if [ "$IS_PRERELEASE" != "true" ]; then
require_result "stable demo deployment and verification" "$DEMO_RESULT" success
fi
+37 -53
View File
@@ -1,12 +1,16 @@
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
# Called explicitly from create-release.yml after the exact Docker image is
# pullable, or dispatched manually for a governed backfill. The release
# pipeline awaits this workflow before customer activation.
on:
workflow_run:
workflows: ["Publish Docker Images"]
types: [completed]
workflow_call:
inputs:
chart_version:
description: "Chart version (e.g., 6.2.0-rc.9)."
required: true
type: string
workflow_dispatch:
inputs:
chart_version:
@@ -19,31 +23,11 @@ permissions:
jobs:
release:
runs-on: ubuntu-24.04
# 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: Determine chart version
id: version
env:
GH_TOKEN: ${{ github.token }}
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
VERSION="${{ inputs.chart_version }}"
else
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
VERSION="${TAG#v}"
fi
VERSION="${{ inputs.chart_version }}"
if [ -z "$VERSION" ]; then
echo "::error::chart_version input is required"
exit 1
@@ -70,12 +54,12 @@ jobs:
--purpose "Helm pages release" \
--github-output "$GITHUB_OUTPUT"
- name: Check out governed release branch
- name: Check out validated release tag
env:
REQUIRED_BRANCH: ${{ steps.release_line.outputs.required_branch }}
RELEASE_TAG: ${{ steps.version.outputs.release_tag }}
run: |
set -euo pipefail
git checkout -B "$REQUIRED_BRANCH" "origin/$REQUIRED_BRANCH"
git checkout --detach "refs/tags/${RELEASE_TAG}"
- name: Configure Git
run: |
@@ -100,27 +84,13 @@ jobs:
helm-docs --version
- name: Generate chart documentation
env:
REQUIRED_BRANCH: ${{ steps.release_line.outputs.required_branch }}
run: |
set -euo pipefail
cd deploy/helm/pulse
helm-docs
# Commit if README changed
if ! git diff --quiet README.md; then
git config user.name "$GITHUB_ACTOR"
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 "$REQUIRED_BRANCH"
git push origin HEAD:"$REQUIRED_BRANCH"
fi
cd ../../..
- name: Update Chart.yaml version
env:
REQUIRED_BRANCH: ${{ steps.release_line.outputs.required_branch }}
run: |
set -euo pipefail
VERSION="${{ steps.version.outputs.version }}"
@@ -129,16 +99,6 @@ jobs:
--version "$VERSION" \
--repo "${{ github.repository }}"
# Commit if Chart.yaml changed
if ! git diff --quiet deploy/helm/pulse/Chart.yaml; then
git config user.name "$GITHUB_ACTOR"
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 "$REQUIRED_BRANCH"
git push origin HEAD:"$REQUIRED_BRANCH"
fi
- name: Validate Helm chart
run: |
# Strict linting
@@ -306,6 +266,30 @@ jobs:
git -C "${workdir}/gh-pages" push origin HEAD:gh-pages
fi
public_repo="https://rcourtman.github.io/Pulse"
public_ready=false
for attempt in $(seq 1 12); do
public_index="${workdir}/public-index.yaml"
if curl -fsSL --retry 3 --retry-delay 2 --retry-all-errors \
-o "$public_index" "${public_repo}/index.yaml" && \
grep -q "version: ${VERSION}" "$public_index"; then
helm repo remove pulse-public >/dev/null 2>&1 || true
helm repo add pulse-public "$public_repo" --force-update
helm repo update pulse-public
if helm show chart pulse-public/pulse --version "$VERSION" >/dev/null; then
public_ready=true
break
fi
fi
echo "Public Helm repository has not exposed ${VERSION} yet (attempt ${attempt}/12)."
sleep 5
done
if [ "$public_ready" != "true" ]; then
echo "::error::Public Helm repository did not expose chart ${VERSION}."
exit 1
fi
echo "[OK] Public Helm repository serves pulse ${VERSION}."
- name: Mark Helm chart release as pre-release (avoid latest override)
env:
GITHUB_TOKEN: ${{ github.token }}
+90 -21
View File
@@ -1,7 +1,7 @@
name: install.sh Smoke (Published Release)
name: install.sh Smoke (Release Assets)
# End-to-end smoke that exercises the documented Proxmox-LXC / systemd
# install flow against the published GitHub Release. Across v6 rc.1 → rc.5
# install flow against staged or published GitHub Release assets. Across v6 rc.1 → rc.5
# the published install.sh asset was actually the agent installer (rejecting
# --version) and the README's pinned signature key did not match the
# pipeline's actual signing key, so anyone running the secure-install
@@ -17,7 +17,8 @@ name: install.sh Smoke (Published Release)
#
# What it does:
# 1. Downloads install.sh + install.sh.sshsig + the linux-amd64 tarball
# from the published release URL (not the local release/ dir).
# from the authenticated draft-release API during a release cut, or from
# the public release URL during a manual post-publication check.
# 2. Extracts the README's pinned ed25519 key and runs the README's exact
# ssh-keygen -Y verify command. Catches README/key drift.
# 3. Boots a privileged systemd Debian 12 container, runs
@@ -40,7 +41,17 @@ on:
required: true
type: string
repository:
description: 'owner/repo to pull the published release from. Defaults to the workflow repository.'
description: 'owner/repo to pull the release from. Defaults to the workflow repository.'
required: false
type: string
default: ''
asset_source:
description: 'Asset source: staged for a draft release, or published for the public release URL.'
required: false
type: string
default: 'published'
release_id:
description: 'Draft release ID. Required when asset_source is staged.'
required: false
type: string
default: ''
@@ -59,6 +70,19 @@ on:
required: false
type: string
default: ''
asset_source:
description: 'Asset source'
required: false
type: choice
options:
- published
- staged
default: published
release_id:
description: 'Draft release ID (required for staged assets)'
required: false
type: string
default: ''
permissions:
contents: read
@@ -80,6 +104,8 @@ jobs:
INPUT_TAG: ${{ inputs.tag }}
INPUT_VERSION: ${{ inputs.version }}
INPUT_REPO: ${{ inputs.repository }}
INPUT_ASSET_SOURCE: ${{ inputs.asset_source }}
INPUT_RELEASE_ID: ${{ inputs.release_id }}
DEFAULT_REPO: ${{ github.repository }}
run: |
# workflow_call and workflow_dispatch both require tag + version,
@@ -91,35 +117,78 @@ jobs:
exit 1
fi
repo="${INPUT_REPO:-$DEFAULT_REPO}"
asset_source="${INPUT_ASSET_SOURCE:-published}"
release_id="${INPUT_RELEASE_ID:-}"
case "$asset_source" in
published) ;;
staged)
if [ -z "$release_id" ]; then
echo "::error::release_id is required when asset_source=staged"
exit 1
fi
;;
*)
echo "::error::asset_source must be staged or published, got: $asset_source"
exit 1
;;
esac
echo "tag=$tag" >> "$GITHUB_OUTPUT"
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "repo=$repo" >> "$GITHUB_OUTPUT"
echo "Resolved: tag=$tag version=$version repo=$repo"
echo "asset_source=$asset_source" >> "$GITHUB_OUTPUT"
echo "release_id=$release_id" >> "$GITHUB_OUTPUT"
echo "Resolved: tag=$tag version=$version repo=$repo asset_source=$asset_source release_id=${release_id:-none}"
- name: Download published install.sh + sshsig + linux-amd64 tarball
- name: Download install.sh + sshsig + linux-amd64 tarball
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ steps.inputs.outputs.tag }}
REPO: ${{ steps.inputs.outputs.repo }}
ASSET_SOURCE: ${{ steps.inputs.outputs.asset_source }}
RELEASE_ID: ${{ steps.inputs.outputs.release_id }}
run: |
set -euo pipefail
mkdir -p smoke-workspace
cd smoke-workspace
base="https://github.com/${REPO}/releases/download/${TAG}"
echo "Pulling from ${base}/"
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors \
-o install.sh "${base}/install.sh"
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors \
-o install.sh.sshsig "${base}/install.sh.sshsig"
tarball="pulse-${TAG}-linux-amd64.tar.gz"
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors \
-o "${tarball}" "${base}/${tarball}"
# The .sshsig sidecar must travel with the tarball: install.sh
# --archive now requires it for signature verification before
# extraction, closing the local-tarball-swap window.
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors \
-o "${tarball}.sshsig" "${base}/${tarball}.sshsig"
assets=(install.sh install.sh.sshsig "${tarball}" "${tarball}.sshsig")
if [ "$ASSET_SOURCE" = "staged" ]; then
assets_json=$(mktemp)
gh api --paginate "repos/${REPO}/releases/${RELEASE_ID}/assets?per_page=100" \
| jq -s 'add' > "$assets_json"
release_state=$(gh api "repos/${REPO}/releases/${RELEASE_ID}" \
--jq '[.tag_name, (.draft | tostring), (.published_at // "")] | @tsv')
actual_tag=$(awk -F '\t' '{print $1}' <<<"$release_state")
is_draft=$(awk -F '\t' '{print $2}' <<<"$release_state")
published_at=$(awk -F '\t' '{print $3}' <<<"$release_state")
if [ "$actual_tag" != "$TAG" ] || [ "$is_draft" != "true" ] || [ -n "$published_at" ]; then
echo "::error::Release ${RELEASE_ID} is not the unpublished draft for ${TAG}."
exit 1
fi
for asset_name in "${assets[@]}"; do
asset_id=$(jq -r --arg name "$asset_name" \
'map(select(.name == $name)) | if length == 1 then .[0].id else empty end' \
"$assets_json")
if [ -z "$asset_id" ]; then
echo "::error::Draft release ${RELEASE_ID} does not contain exactly one ${asset_name} asset."
exit 1
fi
gh api \
-H 'Accept: application/octet-stream' \
"repos/${REPO}/releases/assets/${asset_id}" > "$asset_name"
done
rm -f "$assets_json"
else
base="https://github.com/${REPO}/releases/download/${TAG}"
echo "Pulling from ${base}/"
for asset_name in "${assets[@]}"; do
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors \
-o "$asset_name" "${base}/${asset_name}"
done
fi
echo "Downloaded:"
ls -la
+59 -95
View File
@@ -1,24 +1,16 @@
name: Promote Floating Tags
run-name: Promote Floating Tags ${{ inputs.tag }}
# Promotes floating tags for the Pulse server image after a release. Only
# rcourtman/pulse + ghcr.io/<owner>/pulse are promoted; the Pulse agent
# ships as GitHub Release binaries (see publish-docker.yml line 199), not
# as a Docker image, so there are no agent floating tags to promote.
# Promotes floating tags for the Pulse server and control-plane images at the
# release activation barrier. The exact-version images are staged first by
# publish-docker.yml; this workflow is the sole owner of mutable aliases.
#
# Triggers:
# - workflow_run: chains off publish-docker.yml completion. Historically
# this was the only path; it remains the default for releases that go
# straight through publish-docker.yml.
# - workflow_call: called explicitly from create-release.yml after
# validate_release_assets succeeds. Defensive backup so a workflow_run
# failure doesn't silently leave latest/major/minor tags unpromoted.
# - workflow_call: called explicitly from create-release.yml after the exact
# version images and release assets pass their staging checks.
# - workflow_dispatch: manual backfill path.
on:
workflow_run:
workflows: ["Publish Docker Images"]
types: [completed]
workflow_call:
inputs:
tag:
@@ -52,9 +44,6 @@ permissions:
jobs:
promote-images:
runs-on: ubuntu-24.04
# workflow_dispatch and workflow_call always run; workflow_run only when
# the triggering Publish Docker Images run completed successfully.
if: ${{ github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call' || github.event.workflow_run.conclusion == 'success' }}
permissions:
contents: read
packages: write
@@ -67,40 +56,11 @@ jobs:
INPUT_TAG: ${{ inputs.tag }}
INPUT_PRERELEASE: ${{ inputs.prerelease }}
run: |
# inputs.tag is set by both workflow_dispatch and workflow_call. Fall
# back to the workflow_run derivation only when running as a chain
# downstream of publish-docker.yml.
if [ -n "${INPUT_TAG}" ]; then
TAG="${INPUT_TAG}"
PRERELEASE="${INPUT_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="${INPUT_TAG}"
PRERELEASE="${INPUT_PRERELEASE}"
if [ -z "$TAG" ]; then
echo "::error::tag is required"
exit 1
fi
echo "tag=${TAG}" >> $GITHUB_OUTPUT
@@ -135,26 +95,29 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Wait for Pulse server image to be available
- name: Wait for exact-version images to be available
env:
TAG: ${{ steps.extract.outputs.tag }}
run: |
echo "Waiting for rcourtman/pulse:${TAG} to be available..."
MAX_ATTEMPTS=30
ATTEMPT=0
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!"
exit 0
set -euo pipefail
for image in pulse pulse-control-plane; do
echo "Waiting for rcourtman/${image}:${TAG} to be available..."
available=false
for attempt in $(seq 1 30); do
if docker manifest inspect "rcourtman/${image}:${TAG}" > /dev/null 2>&1; then
echo "Image rcourtman/${image}:${TAG} is available."
available=true
break
fi
echo "Attempt ${attempt}/30 - ${image} image not yet available, waiting 10s..."
sleep 10
done
if [ "$available" != "true" ]; then
echo "::error::Timeout waiting for rcourtman/${image}:${TAG}"
exit 1
fi
ATTEMPT=$((ATTEMPT + 1))
echo "Attempt $ATTEMPT/$MAX_ATTEMPTS - pulse image not yet available, waiting 10s..."
sleep 10
done
echo "Timeout waiting for pulse Docker image"
exit 1
- name: Promote Pulse server image tags
env:
TAG: ${{ steps.extract.outputs.tag }}
@@ -169,40 +132,41 @@ jobs:
MINOR=${MINOR:-0}
MAJOR_MINOR="$MAJOR.$MINOR"
if [ "$PRERELEASE" = "true" ]; then
echo "Promoting prerelease tags for ${TAG}"
docker buildx imagetools create \
-t rcourtman/pulse:rc \
rcourtman/pulse:${TAG}
docker buildx imagetools create \
-t ghcr.io/${OWNER}/pulse:rc \
ghcr.io/${OWNER}/pulse:${TAG}
else
# :latest belongs to the highest stable semver overall. A
# maintenance cut of an older line (e.g. v5.1.36 after v6 GA, or a
# 6.0.x patch after 6.1 ships) must only move its own :MAJOR and
# :MAJOR.MINOR tags.
HIGHEST_STABLE=$(git tag -l 'v*' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -1)
LATEST_ARGS=""
LATEST_ARGS_GHCR=""
HIGHEST_STABLE=$(git tag -l 'v*' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -1)
for image in pulse pulse-control-plane; do
if [ "$PRERELEASE" = "true" ]; then
echo "Promoting :rc for ${image} from ${TAG}."
docker buildx imagetools create \
-t "rcourtman/${image}:rc" \
"rcourtman/${image}:${TAG}"
docker buildx imagetools create \
-t "ghcr.io/${OWNER}/${image}:rc" \
"ghcr.io/${OWNER}/${image}:${TAG}"
continue
fi
docker_hub_tags=(
-t "rcourtman/${image}:${MAJOR_MINOR}"
-t "rcourtman/${image}:${MAJOR}"
)
ghcr_tags=(
-t "ghcr.io/${OWNER}/${image}:${MAJOR_MINOR}"
-t "ghcr.io/${OWNER}/${image}:${MAJOR}"
)
if [ "$TAG" = "$HIGHEST_STABLE" ] || [ "${FORCE_LATEST}" = "true" ]; then
LATEST_ARGS="-t rcourtman/pulse:latest"
LATEST_ARGS_GHCR="-t ghcr.io/${OWNER}/pulse:latest"
echo "Promoting stable tags for ${TAG} (including :latest)"
docker_hub_tags+=( -t "rcourtman/${image}:latest" )
ghcr_tags+=( -t "ghcr.io/${OWNER}/${image}:latest" )
echo "Promoting stable aliases for ${image} from ${TAG}, including :latest."
else
echo "Promoting stable tags for ${TAG} WITHOUT :latest (highest stable is ${HIGHEST_STABLE})"
echo "Promoting stable aliases for ${image} from ${TAG} without :latest (highest stable is ${HIGHEST_STABLE})."
fi
docker buildx imagetools create \
${LATEST_ARGS} \
-t rcourtman/pulse:${MAJOR_MINOR} \
-t rcourtman/pulse:${MAJOR} \
rcourtman/pulse:${TAG}
"${docker_hub_tags[@]}" \
"rcourtman/${image}:${TAG}"
docker buildx imagetools create \
${LATEST_ARGS_GHCR} \
-t ghcr.io/${OWNER}/pulse:${MAJOR_MINOR} \
-t ghcr.io/${OWNER}/pulse:${MAJOR} \
ghcr.io/${OWNER}/pulse:${TAG}
fi
"${ghcr_tags[@]}" \
"ghcr.io/${OWNER}/${image}:${TAG}"
done
- name: Promotion summary
env:
@@ -215,8 +179,8 @@ jobs:
MAJOR_MINOR="$MAJOR.${MINOR:-0}"
if [ "$PRERELEASE" = "true" ]; then
echo "✅ Updated :rc tag to point to ${TAG} for the Pulse server image."
echo "✅ Updated :rc to point to ${TAG} for the server and control-plane images."
else
echo "✅ Updated :latest, :${MAJOR_MINOR}, :${MAJOR} tags to point to ${TAG} for the Pulse server image."
echo "✅ Updated stable floating aliases for the server and control-plane images to ${TAG}."
fi
echo "Pulse Agent ships as GitHub Release binaries, not a Docker image."
+3 -17
View File
@@ -51,7 +51,7 @@ jobs:
IS_PRERELEASE="false"
if [[ "$VERSION" =~ -rc\.[0-9]+$ ]] || [[ "$VERSION" =~ -alpha\.[0-9]+$ ]] || [[ "$VERSION" =~ -beta\.[0-9]+$ ]]; then
IS_PRERELEASE="true"
echo "Detected prerelease version - will NOT update :latest tag"
echo "Detected prerelease version"
fi
echo "tag=${TAG}" >> $GITHUB_OUTPUT
@@ -129,10 +129,8 @@ jobs:
tags: |
rcourtman/pulse:${{ steps.version.outputs.tag }}
rcourtman/pulse:${{ steps.version.outputs.version }}
${{ steps.version.outputs.is_prerelease != 'true' && 'rcourtman/pulse:latest' || '' }}
ghcr.io/${{ github.repository_owner }}/pulse:${{ steps.version.outputs.tag }}
ghcr.io/${{ github.repository_owner }}/pulse:${{ steps.version.outputs.version }}
${{ steps.version.outputs.is_prerelease != 'true' && format('ghcr.io/{0}/pulse:latest', github.repository_owner) || '' }}
- name: Attest Pulse server image on Docker Hub
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
@@ -169,10 +167,8 @@ jobs:
tags: |
rcourtman/pulse-control-plane:${{ steps.version.outputs.tag }}
rcourtman/pulse-control-plane:${{ steps.version.outputs.version }}
${{ steps.version.outputs.is_prerelease != 'true' && 'rcourtman/pulse-control-plane:latest' || '' }}
ghcr.io/${{ github.repository_owner }}/pulse-control-plane:${{ steps.version.outputs.tag }}
ghcr.io/${{ github.repository_owner }}/pulse-control-plane:${{ steps.version.outputs.version }}
${{ steps.version.outputs.is_prerelease != 'true' && format('ghcr.io/{0}/pulse-control-plane:latest', github.repository_owner) || '' }}
- name: Attest Pulse control-plane image on Docker Hub
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
@@ -192,26 +188,16 @@ jobs:
- name: Output image information
run: |
IS_PRERELEASE="${{ steps.version.outputs.is_prerelease }}"
echo "✅ Docker images published successfully!"
echo "✅ Exact-version Docker images staged successfully!"
echo ""
echo "Server images (linux/amd64, linux/arm64):"
echo " - rcourtman/pulse:${{ steps.version.outputs.tag }}"
echo " - rcourtman/pulse:${{ steps.version.outputs.version }}"
if [ "$IS_PRERELEASE" != "true" ]; then
echo " - rcourtman/pulse:latest"
fi
echo ""
echo "Control-plane images (linux/amd64, linux/arm64):"
echo " - rcourtman/pulse-control-plane:${{ steps.version.outputs.tag }}"
echo " - rcourtman/pulse-control-plane:${{ steps.version.outputs.version }}"
if [ "$IS_PRERELEASE" != "true" ]; then
echo " - rcourtman/pulse-control-plane:latest"
fi
echo ""
echo "Pulse Agent binaries ship as release assets, not as a Docker image."
echo "See the GitHub release page for pulse-agent-{darwin,freebsd,linux,windows}-{amd64,arm64,...}."
echo ""
if [ "$IS_PRERELEASE" = "true" ]; then
echo "Note: :latest tags were NOT updated (this is a prerelease)"
fi
echo "Floating aliases are promoted separately at the activation barrier."
+68 -2
View File
@@ -17,6 +17,11 @@ on:
required: false
default: false
type: boolean
release_id:
description: 'Optional staged GitHub release ID used before customer activation'
required: false
default: ''
type: string
workflow_dispatch:
inputs:
tag:
@@ -36,6 +41,11 @@ on:
required: false
default: false
type: boolean
release_id:
description: 'Optional staged GitHub release ID'
required: false
default: ''
type: string
permissions:
contents: read
@@ -193,9 +203,43 @@ jobs:
- name: Wait for release assets
if: inputs.verify_only != true
env:
GH_TOKEN: ${{ github.token }}
RELEASE_ID: ${{ inputs.release_id }}
run: |
set -euo pipefail
TAG="${{ needs.resolve.outputs.tag }}"
if [ -n "${RELEASE_ID:-}" ]; then
release_state=$(gh api "repos/${{ github.repository }}/releases/${RELEASE_ID}" \
--jq '[.tag_name, (.draft | tostring), (.published_at // "")] | @tsv')
actual_tag=$(awk -F '\t' '{print $1}' <<<"$release_state")
is_draft=$(awk -F '\t' '{print $2}' <<<"$release_state")
published_at=$(awk -F '\t' '{print $3}' <<<"$release_state")
if [ "$actual_tag" != "$TAG" ] || [ "$is_draft" != "true" ] || [ -n "$published_at" ]; then
echo "::error::Release ${RELEASE_ID} is not the unpublished draft for ${TAG}."
exit 1
fi
assets_json=$(mktemp)
gh api --paginate "repos/${{ github.repository }}/releases/${RELEASE_ID}/assets?per_page=100" \
| jq -s 'add' > "$assets_json"
tarball="pulse-${TAG}-linux-amd64.tar.gz"
for asset_name in "$tarball" "${tarball}.sshsig"; do
asset_id=$(jq -r --arg name "$asset_name" \
'map(select(.name == $name)) | if length == 1 then .[0].id else empty end' \
"$assets_json")
if [ -z "$asset_id" ]; then
echo "::error::Draft release ${RELEASE_ID} does not contain exactly one ${asset_name} asset."
exit 1
fi
gh api -H 'Accept: application/octet-stream' \
"repos/${{ github.repository }}/releases/assets/${asset_id}" > "/tmp/${asset_name}"
done
rm -f "$assets_json"
echo "Staged release assets are available for the demo update."
exit 0
fi
echo "Waiting for release assets to be available..."
MAX_ATTEMPTS=30
@@ -428,15 +472,24 @@ jobs:
env:
DEMO_SERVER_HOST: ${{ secrets.DEMO_SERVER_HOST }}
DEMO_SERVER_USER: ${{ secrets.DEMO_SERVER_USER }}
RELEASE_ID: ${{ inputs.release_id }}
run: |
set -euo pipefail
scp -i ~/.ssh/id_ed25519 /tmp/pulse-install.sh "$DEMO_SERVER_USER@$DEMO_SERVER_HOST:/tmp/pulse-install.sh"
if [ -n "${RELEASE_ID:-}" ]; then
TAG="${{ needs.resolve.outputs.tag }}"
tarball="pulse-${TAG}-linux-amd64.tar.gz"
scp -i ~/.ssh/id_ed25519 \
"/tmp/${tarball}" "/tmp/${tarball}.sshsig" \
"$DEMO_SERVER_USER@$DEMO_SERVER_HOST:/tmp/"
fi
- name: Update demo server
if: inputs.verify_only != true && steps.current.outputs.skip_current != 'true'
env:
DEMO_SERVER_HOST: ${{ secrets.DEMO_SERVER_HOST }}
DEMO_SERVER_USER: ${{ secrets.DEMO_SERVER_USER }}
RELEASE_ID: ${{ inputs.release_id }}
run: |
set -euo pipefail
TAG="${{ needs.resolve.outputs.tag }}"
@@ -445,15 +498,28 @@ jobs:
set -euo pipefail
TAG="$1"
SERVICE_NAME="$2"
USE_STAGED_ARCHIVE="$3"
INSTALLER_ENV=()
if [ -n "$SERVICE_NAME" ]; then
INSTALLER_ENV+=("PULSE_SERVICE_NAME=$SERVICE_NAME")
fi
sudo env "${INSTALLER_ENV[@]}" bash /tmp/pulse-install.sh --version "$TAG"
if [ "$USE_STAGED_ARCHIVE" = "true" ]; then
tarball="pulse-${TAG}-linux-amd64.tar.gz"
sudo env "${INSTALLER_ENV[@]}" bash /tmp/pulse-install.sh \
--archive "/tmp/${tarball}" --disable-auto-updates
rm -f "/tmp/${tarball}" "/tmp/${tarball}.sshsig"
else
sudo env "${INSTALLER_ENV[@]}" bash /tmp/pulse-install.sh --version "$TAG"
fi
rm -f /tmp/pulse-install.sh
EOF
)
ssh -i ~/.ssh/id_ed25519 "$DEMO_SERVER_USER@$DEMO_SERVER_HOST" "bash -s -- $(printf '%q ' "$TAG" "$SERVICE_NAME")" <<<"$REMOTE_SCRIPT"
use_staged_archive=false
if [ -n "${RELEASE_ID:-}" ]; then
use_staged_archive=true
fi
ssh -i ~/.ssh/id_ed25519 "$DEMO_SERVER_USER@$DEMO_SERVER_HOST" \
"bash -s -- $(printf '%q ' "$TAG" "$SERVICE_NAME" "$use_staged_archive")" <<<"$REMOTE_SCRIPT"
- name: Restore demo runtime configuration
if: inputs.verify_only != true