mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-24 04:33:17 +00:00
042b01a953
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.
256 lines
8.5 KiB
Bash
Executable File
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>"
|