Files
pulse/scripts/trigger-release-dry-run.sh
T
Richard Courtman 042b01a953 Soak release candidates for 24 hours before stable
The resolver refused a stable promotion until its candidate had soaked for
72 hours, or 168 for a minor. Few installs run previews: 35 of about 7,900
active installs ran v6.4.5-rc.3 in the week to 2026-09-28, so a longer
soak saw little the first day did not, while 75 percent of installs sat on
a stable build a month old. Richard set a 24 hour soak for every release
on 2026-09-28.

The minimums drop to 24 hours for minor and patch alike, the error text
reads the constants instead of repeating numbers, and the workflow and
trigger prompts, the promotion policy and the deployment contract say the
same. The content-drift check and the hotfix exception are unchanged.
2026-09-28 16:42:46 +01:00

256 lines
8.5 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
# Helper script to trigger the governed non-publish release rehearsal.
# Usage: ./scripts/trigger-release-dry-run.sh 6.0.0
VERSION="${1:-}"
if [ -z "$VERSION" ]; then
echo "Error: Version number required"
echo "Usage: $0 <version>"
echo "Example: $0 6.0.0"
exit 1
fi
echo "Pre-flight checks for Release Dry Run v${VERSION}..."
echo ""
IS_PRERELEASE="false"
RELEASE_STAGE="stable"
if [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+-(alpha|beta|rc)\.([1-9][0-9]*)$ ]]; then
IS_PRERELEASE="true"
RELEASE_STAGE="${BASH_REMATCH[1]}"
elif [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "❌ Unsupported release version: ${VERSION}"
echo "Use X.Y.Z, X.Y.Z-alpha.N, X.Y.Z-beta.N, or X.Y.Z-rc.N."
exit 1
fi
case "$RELEASE_STAGE" in
alpha) echo "Maturity: alpha (incomplete internal evaluation build)" ;;
beta) echo "Maturity: beta (user-testing build that is not yet release-ready)" ;;
rc) echo "Maturity: release candidate (believed capable of becoming stable without product changes)" ;;
stable) echo "Maturity: stable (general production release)" ;;
esac
FILE_VERSION=$(cat VERSION | tr -d '\n')
if [ "$FILE_VERSION" != "$VERSION" ]; then
echo "❌ VERSION file mismatch"
echo ""
echo " VERSION file contains: ${FILE_VERSION}"
echo " Requested version: ${VERSION}"
echo ""
echo "Fix: update VERSION to the governed rehearsal candidate before dispatching."
exit 1
fi
echo "✓ VERSION file matches (${VERSION})"
if ! git diff-index --quiet HEAD --; then
echo "❌ Working directory has uncommitted changes"
echo ""
echo "Fix: Commit or stash changes before running the rehearsal"
echo ""
git status --short
exit 1
fi
echo "✓ Working directory is clean"
CURRENT_BRANCH=$(git branch --show-current)
REQUIRED_BRANCH=$(python3 scripts/release_control/control_plane.py --branch-for-version "$VERSION")
if [ "$CURRENT_BRANCH" != "$REQUIRED_BRANCH" ]; then
echo "❌ Wrong release branch"
echo ""
echo " Current branch: ${CURRENT_BRANCH}"
echo " Required branch: ${REQUIRED_BRANCH}"
echo ""
exit 1
fi
echo "✓ On required branch (${REQUIRED_BRANCH})"
git fetch origin --quiet
LOCAL=$(git rev-parse @)
REMOTE=$(git rev-parse '@{u}')
if [ "$LOCAL" != "$REMOTE" ]; then
echo "❌ Local branch is not fully pushed to origin"
echo ""
echo " Local ref: ${LOCAL}"
echo " Remote ref: ${REMOTE}"
echo ""
echo "Release Dry Run executes the selected remote ref, not local-only governance state."
echo "Push ${CURRENT_BRANCH} to origin before dispatching the rehearsal."
git status -sb
exit 1
else
echo "✓ Up to date with remote"
fi
./scripts/run-release-preflight.sh \
--sha "$LOCAL" \
--profile rehearsal \
--if-configured
python3 scripts/check-workflow-dispatch-inputs.py \
--workflow-path .github/workflows/release-dry-run.yml \
--branch "$CURRENT_BRANCH" \
--require version \
--require promoted_from_tag \
--require rollback_version \
--require ga_date \
--require v5_eos_date \
--require hotfix_exception \
--require hotfix_reason \
--require unsigned_windows_exception \
--require unsigned_windows_reason \
--require note \
--require mobile_release_decision \
--require mobile_release_evidence
echo "✓ Remote release-branch dry-run workflow contract matches governed inputs"
MOBILE_RELEASE_DECISION=""
MOBILE_RELEASE_EVIDENCE=""
MOBILE_REPO="../pulse-mobile"
echo ""
python3 scripts/release_control/mobile_release_gate.py --mobile-repo "$MOBILE_REPO" --summary-only || true
echo ""
echo "Mobile release decision:"
echo " no-mobile-impact Server release has no mobile/relay/onboarding compatibility impact."
echo " existing-mobile-build-compatible Existing TestFlight/Play candidate is proven compatible."
echo " mobile-candidate-uploaded A new mobile candidate has already been uploaded."
echo " mobile-candidate-required A mobile candidate is required; stop this release dispatch."
echo ""
read -r -p "Mobile release decision: " MOBILE_RELEASE_DECISION
read -r -p "Mobile release evidence or note: " MOBILE_RELEASE_EVIDENCE
python3 scripts/release_control/mobile_release_gate.py \
--version "$VERSION" \
--decision "$MOBILE_RELEASE_DECISION" \
--evidence "$MOBILE_RELEASE_EVIDENCE"
echo "✓ Mobile release decision recorded"
ROLLBACK_VERSION=""
PROMOTED_FROM_TAG=""
GA_DATE=""
V5_EOS_DATE=""
HOTFIX_EXCEPTION="false"
HOTFIX_REASON=""
UNSIGNED_WINDOWS_EXCEPTION="false"
UNSIGNED_WINDOWS_REASON=""
echo ""
read -r -p "Rollback stable version (for example 5.1.14 or v5.1.14): " ROLLBACK_VERSION
if [ -z "$ROLLBACK_VERSION" ]; then
echo "❌ Error: rollback version is required"
exit 1
fi
if [ "$IS_PRERELEASE" != "true" ]; then
echo ""
read -r -p "Hotfix exception to bypass 24-hour prerelease soak? [y/N] " HOTFIX_REPLY
if [[ "$HOTFIX_REPLY" =~ ^[Yy]$ ]]; then
HOTFIX_EXCEPTION="true"
read -r -p "Hotfix reason: " HOTFIX_REASON
if [ -z "$HOTFIX_REASON" ]; then
echo "❌ Error: hotfix reason is required when bypassing prerelease soak"
exit 1
fi
fi
DEFAULT_PROMOTED_FROM_TAG=$(git tag -l "v${VERSION}-rc.*" --sort=-version:refname | head -1 || true)
echo ""
if [ -n "$DEFAULT_PROMOTED_FROM_TAG" ]; then
if [ "$HOTFIX_EXCEPTION" = "true" ]; then
read -r -p "Promoted prerelease tag, or blank for hotfix with no RC lineage [${DEFAULT_PROMOTED_FROM_TAG}]: " PROMOTED_FROM_TAG
else
read -r -p "Promoted prerelease tag [${DEFAULT_PROMOTED_FROM_TAG}]: " PROMOTED_FROM_TAG
PROMOTED_FROM_TAG="${PROMOTED_FROM_TAG:-$DEFAULT_PROMOTED_FROM_TAG}"
fi
else
read -r -p "Promoted prerelease tag (for example v${VERSION}-rc.2; blank only for approved hotfix): " PROMOTED_FROM_TAG
fi
if [ -z "$PROMOTED_FROM_TAG" ] && [ "$HOTFIX_EXCEPTION" != "true" ]; then
echo "❌ Error: promoted prerelease tag is required for stable rehearsals unless this is an approved hotfix"
exit 1
fi
if [ "$VERSION" = "6.0.0" ]; then
echo ""
read -r -p "v6 GA date to publish with GA (YYYY-MM-DD): " GA_DATE
if [[ ! "$GA_DATE" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
echo "❌ Error: v6 GA date must be in YYYY-MM-DD form"
exit 1
fi
echo ""
read -r -p "v5 end-of-support date to publish with GA (YYYY-MM-DD): " V5_EOS_DATE
if [[ ! "$V5_EOS_DATE" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
echo "❌ Error: v5 end-of-support date must be in YYYY-MM-DD form"
exit 1
fi
fi
if [[ "$VERSION" =~ ^6\.(1\.[012]|2\.0)$ ]]; then
echo ""
read -r -p "Use the recorded v${VERSION} unsigned Windows exception? [y/N] " UNSIGNED_WINDOWS_REPLY
if [[ "$UNSIGNED_WINDOWS_REPLY" =~ ^[Yy]$ ]]; then
UNSIGNED_WINDOWS_EXCEPTION="true"
read -r -p "Unsigned Windows exception reason: " UNSIGNED_WINDOWS_REASON
if [ -z "$UNSIGNED_WINDOWS_REASON" ]; then
echo "❌ Error: an owner reason is required for the unsigned Windows exception"
exit 1
fi
fi
fi
fi
echo ""
echo "Resolving governed promotion metadata..."
RESOLVER_ARGS=(
--version "$VERSION"
--rollback-version "$ROLLBACK_VERSION"
)
if [ -n "$PROMOTED_FROM_TAG" ]; then
RESOLVER_ARGS+=(--promoted-from-tag "$PROMOTED_FROM_TAG")
fi
if [ -n "$GA_DATE" ]; then
RESOLVER_ARGS+=(--ga-date "$GA_DATE")
fi
if [ -n "$V5_EOS_DATE" ]; then
RESOLVER_ARGS+=(--v5-eos-date "$V5_EOS_DATE")
fi
if [ "$HOTFIX_EXCEPTION" = "true" ]; then
RESOLVER_ARGS+=(--hotfix-exception --hotfix-reason "$HOTFIX_REASON")
fi
if [ "$UNSIGNED_WINDOWS_EXCEPTION" = "true" ]; then
RESOLVER_ARGS+=(
--unsigned-windows-exception
--unsigned-windows-reason "$UNSIGNED_WINDOWS_REASON"
)
fi
python3 scripts/release_control/resolve_release_promotion.py "${RESOLVER_ARGS[@]}" >/tmp/pulse-release-dry-run-metadata.out
cat /tmp/pulse-release-dry-run-metadata.out
echo ""
echo "Triggering Release Dry Run workflow..."
gh workflow run release-dry-run.yml \
--ref "$CURRENT_BRANCH" \
-f version="$VERSION" \
-f promoted_from_tag="$PROMOTED_FROM_TAG" \
-f rollback_version="$ROLLBACK_VERSION" \
-f ga_date="$GA_DATE" \
-f v5_eos_date="$V5_EOS_DATE" \
-f hotfix_exception="$HOTFIX_EXCEPTION" \
-f hotfix_reason="$HOTFIX_REASON" \
-f unsigned_windows_exception="$UNSIGNED_WINDOWS_EXCEPTION" \
-f unsigned_windows_reason="$UNSIGNED_WINDOWS_REASON" \
-f note="Governed release rehearsal for ${VERSION}" \
-f mobile_release_decision="$MOBILE_RELEASE_DECISION" \
-f mobile_release_evidence="$MOBILE_RELEASE_EVIDENCE"
echo ""
echo "✓ Release Dry Run workflow triggered"
echo ""
echo "Monitor progress:"
echo " gh run list --workflow=release-dry-run.yml --limit 1"
echo " gh run watch <run-id>"