Pulse/scripts/trigger-stable-patch.sh

201 lines
6.2 KiB
Bash
Executable file

#!/usr/bin/env bash
set -euo pipefail
MODE="publish"
VERSION=""
MOBILE_RELEASE_DECISION=""
MOBILE_RELEASE_EVIDENCE=""
HOTFIX_REASON=""
usage() {
cat <<'EOF'
Usage: scripts/trigger-stable-patch.sh [--dry-run] [options] [version]
Dispatches exactly one governed workflow. The default release workflow builds
and validates an immutable candidate before publication. Use --dry-run only
when a no-public-release rehearsal is required.
Options:
--dry-run Dispatch Release Dry Run only.
--mobile-release-decision VALUE Override the inferred mobile decision.
--mobile-release-evidence VALUE Evidence for a mobile compatibility decision.
--emergency-hotfix-reason VALUE Bypass an RC-required risk with an explicit reason.
-h, --help Show this help.
EOF
}
while [ "$#" -gt 0 ]; do
case "$1" in
--dry-run)
MODE="dry-run"
shift
;;
--mobile-release-decision)
MOBILE_RELEASE_DECISION="${2:?--mobile-release-decision requires a value}"
shift 2
;;
--mobile-release-evidence)
MOBILE_RELEASE_EVIDENCE="${2:?--mobile-release-evidence requires a value}"
shift 2
;;
--emergency-hotfix-reason)
HOTFIX_REASON="${2:?--emergency-hotfix-reason requires a value}"
shift 2
;;
-h|--help)
usage
exit 0
;;
--*)
echo "Unknown option: $1" >&2
usage >&2
exit 2
;;
*)
if [ -n "$VERSION" ]; then
echo "Only one version may be supplied." >&2
exit 2
fi
VERSION="$1"
shift
;;
esac
done
VERSION="${VERSION:-$(tr -d '\n' < VERSION)}"
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[1-9][0-9]*$ ]]; then
echo "Stable patch version required, got: ${VERSION}" >&2
exit 1
fi
FILE_VERSION="$(tr -d '\n' < VERSION)"
if [ "$FILE_VERSION" != "$VERSION" ]; then
echo "VERSION contains ${FILE_VERSION}; requested ${VERSION}." >&2
exit 1
fi
if [ -n "$(git status --porcelain=v1)" ]; then
echo "The release worktree must be clean." >&2
git status --short
exit 1
fi
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 "Version ${VERSION} must be released from ${REQUIRED_BRANCH}, not ${CURRENT_BRANCH}." >&2
exit 1
fi
git fetch --quiet --prune origin "$REQUIRED_BRANCH" --tags
LOCAL_SHA="$(git rev-parse HEAD)"
REMOTE_SHA="$(git rev-parse "origin/${REQUIRED_BRANCH}")"
if [ "$LOCAL_SHA" != "$REMOTE_SHA" ]; then
echo "The exact release commit must already be pushed to origin/${REQUIRED_BRANCH}." >&2
exit 1
fi
NOTES_FILE="docs/releases/RELEASE_NOTES_v${VERSION}.md"
if [ ! -s "$NOTES_FILE" ]; then
echo "Canonical release notes are required at ${NOTES_FILE}." >&2
exit 1
fi
RESOLVER_ARGS=(
--version "$VERSION"
--derive-rollback-latest-stable
--release-notes-file "$NOTES_FILE"
)
HOTFIX_EXCEPTION="false"
if [ -n "$HOTFIX_REASON" ]; then
HOTFIX_EXCEPTION="true"
RESOLVER_ARGS+=(--hotfix-exception --hotfix-reason "$HOTFIX_REASON")
fi
PROMOTION_METADATA="$(python3 scripts/release_control/resolve_release_promotion.py "${RESOLVER_ARGS[@]}")"
ROLLBACK_TAG="$(awk -F= '$1 == "rollback_tag" {print $2}' <<<"$PROMOTION_METADATA")"
if [ -z "$ROLLBACK_TAG" ]; then
echo "Release preflight did not resolve a rollback tag." >&2
exit 1
fi
MOBILE_IMPACT_PATHS="$({
git diff --name-only "${ROLLBACK_TAG}..HEAD" | rg '^(internal/(relay|mobile)/|pkg/relay/|internal/api/(cloud_handoff|magic_link|mobile)|tests/integration/.*(mobile|relay)|scripts/.*mobile)'
} || true)"
if [ -z "$MOBILE_RELEASE_DECISION" ]; then
if [ -n "$MOBILE_IMPACT_PATHS" ]; then
echo "Mobile-facing paths changed since ${ROLLBACK_TAG}:" >&2
printf '%s\n' "$MOBILE_IMPACT_PATHS" >&2
echo "Supply --mobile-release-decision and --mobile-release-evidence after completing the governed mobile check." >&2
exit 1
fi
MOBILE_RELEASE_DECISION="no-mobile-impact"
MOBILE_RELEASE_EVIDENCE="No mobile-facing paths changed between ${ROLLBACK_TAG} and ${LOCAL_SHA}."
fi
python3 scripts/release_control/mobile_release_gate.py \
--version "$VERSION" \
--decision "$MOBILE_RELEASE_DECISION" \
--evidence "$MOBILE_RELEASE_EVIDENCE"
if [ "$MODE" = "dry-run" ]; then
WORKFLOW="release-dry-run.yml"
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 note \
--require mobile_release_decision \
--require mobile_release_evidence
gh workflow run "$WORKFLOW" \
--ref "$CURRENT_BRANCH" \
-f version="$VERSION" \
-f promoted_from_tag="" \
-f rollback_version="$ROLLBACK_TAG" \
-f ga_date="" \
-f v5_eos_date="" \
-f hotfix_exception="$HOTFIX_EXCEPTION" \
-f hotfix_reason="$HOTFIX_REASON" \
-f note="Stable patch preflight for ${VERSION} at ${LOCAL_SHA}" \
-f mobile_release_decision="$MOBILE_RELEASE_DECISION" \
-f mobile_release_evidence="$MOBILE_RELEASE_EVIDENCE"
else
python3 scripts/check-workflow-dispatch-inputs.py \
--workflow-path .github/workflows/create-release.yml \
--branch "$CURRENT_BRANCH" \
--require version \
--require release_notes \
--require promoted_from_tag \
--require rollback_version \
--require ga_date \
--require v5_eos_date \
--require hotfix_exception \
--require hotfix_reason \
--require draft_only \
--require mobile_release_decision \
--require mobile_release_evidence
gh workflow run create-release.yml \
--ref "$CURRENT_BRANCH" \
-f version="$VERSION" \
-f release_notes="$(<"$NOTES_FILE")" \
-f promoted_from_tag="" \
-f rollback_version="$ROLLBACK_TAG" \
-f ga_date="" \
-f v5_eos_date="" \
-f hotfix_exception="$HOTFIX_EXCEPTION" \
-f hotfix_reason="$HOTFIX_REASON" \
-f draft_only=false \
-f mobile_release_decision="$MOBILE_RELEASE_DECISION" \
-f mobile_release_evidence="$MOBILE_RELEASE_EVIDENCE"
fi
echo "Dispatched ${WORKFLOW:-create-release.yml} for v${VERSION} at ${LOCAL_SHA}."