Files
pulse/.github/workflows/promote-private-pro-runtime.yml
pulse-triage[bot] a34f3c752b Keep convergence evidence off sealed releases
Change-source: pulse-maintainer
2026-09-01 11:38:55 +01:00

196 lines
8.5 KiB
YAML

name: Promote Private Pro Runtime
run-name: Promote Private Pro Runtime ${{ inputs.tag }}
on:
workflow_call:
inputs:
version:
description: "Exact Pulse version without the leading v."
required: true
type: string
tag:
description: "Exact Pulse release tag."
required: true
type: string
prerelease:
description: "Whether this release targets the RC broker slot."
required: true
type: boolean
r2_prefix:
description: "Signed private Pro packet prefix staged before activation."
required: true
type: string
pulse_lease_sha:
description: "Exact customer-promotion lease commit owned by this convergence run."
required: true
type: string
pulse_convergence_run_id:
description: "Exact Release Convergence run that owns the lease."
required: true
type: string
pulse_owner_asset_name:
description: "Owner evidence path in the exact Pulse lease commit."
required: true
type: string
pulse_owner_asset_sha256:
description: "SHA-256 digest of the commit-bound owner evidence."
required: true
type: string
permissions:
contents: read
jobs:
promote:
runs-on: ubuntu-24.04
timeout-minutes: 65
steps:
- name: Promote and verify private Pro runtime
env:
GH_TOKEN: ${{ secrets.WORKFLOW_PAT }}
VERSION: ${{ inputs.version }}
TAG: ${{ inputs.tag }}
IS_PRERELEASE: ${{ inputs.prerelease }}
R2_PREFIX: ${{ inputs.r2_prefix }}
PULSE_LEASE_SHA: ${{ inputs.pulse_lease_sha }}
PULSE_CONVERGENCE_RUN_ID: ${{ inputs.pulse_convergence_run_id }}
PULSE_OWNER_ASSET_NAME: ${{ inputs.pulse_owner_asset_name }}
PULSE_OWNER_ASSET_SHA256: ${{ inputs.pulse_owner_asset_sha256 }}
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
if [[ ! "${TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta|rc)\.[1-9][0-9]*)?$ ]] || \
[[ ! "${PULSE_LEASE_SHA}" =~ ^[0-9a-f]{40}$ ]] || \
[[ ! "${PULSE_CONVERGENCE_RUN_ID}" =~ ^[0-9]+$ ]] || \
[[ ! "${PULSE_OWNER_ASSET_NAME}" =~ ^release-convergence-owner-${PULSE_CONVERGENCE_RUN_ID}-[1-9][0-9]*\.json$ ]] || \
[[ ! "${PULSE_OWNER_ASSET_SHA256}" =~ ^[0-9a-f]{64}$ ]]; then
echo "::error::Paid-runtime promotion requires an exact release, convergence owner, and lease."
exit 1
fi
release_json="$(mktemp)"
marker="$(mktemp)"
owner_record="$(mktemp)"
cleanup_owner_proof() {
rm -f "${release_json}" "${marker}" "${owner_record}"
}
trap cleanup_owner_proof EXIT
gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${TAG}" > "${release_json}"
jq -e --arg tag "${TAG}" \
'.tag_name == $tag and .draft == false and .immutable == true and (.published_at | type == "string" and length > 0)' \
"${release_json}" >/dev/null
observed_lease_sha="$(
gh api \
"repos/${GITHUB_REPOSITORY}/git/ref/heads/release-customer-promotion-lock" \
--jq '.object.sha'
)"
if [[ "${observed_lease_sha}" != "${PULSE_LEASE_SHA}" ]]; then
echo "::error::Paid-runtime lease is ${observed_lease_sha:-absent}, expected ${PULSE_LEASE_SHA}."
exit 1
fi
curl -fsSL --retry 4 --retry-delay 2 --retry-all-errors \
-o "${marker}" \
"https://github.com/${GITHUB_REPOSITORY}/releases/download/${TAG}/release-activation.json"
curl -fsSL --retry 4 --retry-delay 2 --retry-all-errors \
-o "${owner_record}" \
"https://raw.githubusercontent.com/${GITHUB_REPOSITORY}/${PULSE_LEASE_SHA}/${PULSE_OWNER_ASSET_NAME}"
printf '%s %s\n' "${PULSE_OWNER_ASSET_SHA256}" "${owner_record}" | sha256sum --check --
activation_owner_run_id="$(jq -r '.convergence_run_id' "${marker}")"
activation_marker_sha256="$(sha256sum "${marker}" | awk '{print $1}')"
jq -e \
--arg tag "${TAG}" \
--arg r2_prefix "${R2_PREFIX}" \
--arg activation_owner_run_id "${activation_owner_run_id}" \
--arg activation_marker_sha256 "${activation_marker_sha256}" \
--arg convergence_run_id "${PULSE_CONVERGENCE_RUN_ID}" \
--arg owner_asset_name "${PULSE_OWNER_ASSET_NAME}" \
'.schema_version == 2 and .tag == $tag and .r2_prefix == $r2_prefix and .activation_owner_run_id == $activation_owner_run_id and .activation_marker_sha256 == $activation_marker_sha256 and .convergence_run_id == $convergence_run_id and .owner_asset_name == $owner_asset_name' \
"${owner_record}" >/dev/null
cleanup_owner_proof
trap - EXIT
echo "[OK] Paid-runtime mutation is bound to immutable ${TAG} convergence run ${PULSE_CONVERGENCE_RUN_ID}."
wait_for_workflow() {
local repo="$1"
local run_id="$2"
local label="$3"
local timeout_seconds="$4"
local deadline=$((SECONDS + timeout_seconds))
if [[ ! "${run_id}" =~ ^[0-9]+$ ]]; then
echo "::error::Dispatch for ${label} did not return an exact workflow run ID."
return 1
fi
echo "Watching exact ${label} run ${run_id} in ${repo}."
while (( SECONDS < deadline )); do
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
sleep 5
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
echo "Dispatching live paid-runtime promotion for ${TAG} with R2 prefix ${R2_PREFIX}."
promote_dispatch="$(
jq -n \
--arg version "${VERSION}" \
--arg r2_prefix "${R2_PREFIX}" \
--arg allow_ga_prefix "${allow_ga_publish}" \
--arg pulse_lease_sha "${PULSE_LEASE_SHA}" \
--arg pulse_convergence_run_id "${PULSE_CONVERGENCE_RUN_ID}" \
--arg pulse_owner_asset_name "${PULSE_OWNER_ASSET_NAME}" \
--arg pulse_owner_asset_sha256 "${PULSE_OWNER_ASSET_SHA256}" \
'{
ref: "main",
return_run_details: true,
inputs: {
version: $version,
r2_prefix: $r2_prefix,
allow_ga_prefix: $allow_ga_prefix,
pulse_lease_sha: $pulse_lease_sha,
pulse_convergence_run_id: $pulse_convergence_run_id,
pulse_owner_asset_name: $pulse_owner_asset_name,
pulse_owner_asset_sha256: $pulse_owner_asset_sha256
}
}' | \
gh api \
--method POST \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2026-03-10" \
repos/rcourtman/pulse-pro/actions/workflows/promote-paid-runtime-release.yml/dispatches \
--input -
)"
promote_run_id="$(jq -r '.workflow_run_id // empty' <<<"${promote_dispatch}")"
wait_for_workflow rcourtman/pulse-pro "${promote_run_id}" "private Pro live promotion" 3600