mirror of
https://github.com/lightningnetwork/lnd.git
synced 2026-08-13 12:32:48 +02:00
scripts: add tag-release.sh to safely cut release tags
Adds a script that creates a signed annotated release tag only after
verifying:
1. The requested tag name matches the version constants committed in
HEAD:build/version.go. Catches the failure mode where a release
branch is tagged before the version bump has been committed, which
would otherwise leave the tagged commit reporting an old version
string at runtime.
2. The local HEAD is identical to the upstream lightningnetwork/lnd
view of the release branch. A release tag must never point at a
commit that has not been merged upstream yet.
The upstream remote is discovered by URL rather than by name, since
"origin" is conventionally the fork in a "gh repo fork" workflow. The
branch defaults to whichever one is currently checked out (typically a
release branch such as v0.21.x-branch) and can be overridden with
--branch.
The script deliberately does not push the tag or auto-bump version.go;
both remain explicit human steps.
(cherry picked from commit 5320aa0e36)
This commit is contained in:
parent
fad437d99d
commit
f600fcaf6d
1 changed files with 150 additions and 0 deletions
150
scripts/tag-release.sh
Executable file
150
scripts/tag-release.sh
Executable file
|
|
@ -0,0 +1,150 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# tag-release.sh creates a signed annotated git tag for an lnd release after
|
||||
# verifying (a) HEAD is in sync with the upstream lightningnetwork/lnd
|
||||
# branch, and (b) build/version.go at HEAD matches the requested tag. Guards
|
||||
# against tagging a commit that has not been merged upstream yet, or one
|
||||
# whose embedded version disagrees with the tag.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
VERSION_FILE="build/version.go"
|
||||
|
||||
# Match the canonical upstream URL across https / git@ / ssh:// forms, with or
|
||||
# without a `.git` suffix. We identify the remote by URL because `origin` is
|
||||
# conventionally the fork in a `gh repo fork` setup.
|
||||
UPSTREAM_URL_REGEX='[:/]lightningnetwork/lnd(\.git)?$'
|
||||
|
||||
usage() {
|
||||
cat >&2 <<EOF
|
||||
Usage: $(basename "$0") <tag-name> [--branch <branch>]
|
||||
|
||||
<tag-name> Release tag, e.g. v0.21.0-beta.rc3. Must match the
|
||||
constants defined in ${VERSION_FILE} at HEAD.
|
||||
--branch <branch> Upstream branch to verify HEAD against. Defaults to
|
||||
the currently checked-out branch (typically a release
|
||||
branch such as v0.21.x-branch).
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
TAG=""
|
||||
UPSTREAM_BRANCH=""
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help) usage ;;
|
||||
--branch) [[ $# -ge 2 ]] || usage; UPSTREAM_BRANCH="$2"; shift 2 ;;
|
||||
--branch=*) UPSTREAM_BRANCH="${1#--branch=}"; shift ;;
|
||||
-*) echo "Unknown flag: $1" >&2; usage ;;
|
||||
*) [[ -z "${TAG}" ]] || usage; TAG="$1"; shift ;;
|
||||
esac
|
||||
done
|
||||
[[ -n "${TAG}" ]] || usage
|
||||
|
||||
cd "$(git rev-parse --show-toplevel)"
|
||||
|
||||
if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then
|
||||
echo "Error: tag ${TAG} already exists locally." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "${UPSTREAM_BRANCH}" ]]; then
|
||||
UPSTREAM_BRANCH="$(git symbolic-ref --quiet --short HEAD || true)"
|
||||
[[ -n "${UPSTREAM_BRANCH}" ]] \
|
||||
|| { echo "Error: detached HEAD; pass --branch <name>." >&2; exit 1; }
|
||||
fi
|
||||
|
||||
# Discover the upstream remote by URL (see UPSTREAM_URL_REGEX).
|
||||
UPSTREAM_REMOTES=()
|
||||
while IFS= read -r line; do
|
||||
UPSTREAM_REMOTES+=("$line")
|
||||
done < <(git remote -v | awk -v re="${UPSTREAM_URL_REGEX}" \
|
||||
'$3 == "(fetch)" && $2 ~ re { print $1 }' | sort -u)
|
||||
|
||||
case "${#UPSTREAM_REMOTES[@]}" in
|
||||
0) echo "Error: no git remote points at lightningnetwork/lnd. Add one with" \
|
||||
"'git remote add upstream" \
|
||||
"https://github.com/lightningnetwork/lnd.git'." >&2
|
||||
exit 1 ;;
|
||||
1) UPSTREAM_REMOTE="${UPSTREAM_REMOTES[0]}" ;;
|
||||
*) echo "Error: multiple remotes match lightningnetwork/lnd:" >&2
|
||||
printf ' %s\n' "${UPSTREAM_REMOTES[@]}" >&2
|
||||
exit 1 ;;
|
||||
esac
|
||||
|
||||
# Fetch first so every later check runs against confirmed-current upstream
|
||||
# state. Without this, a stale local HEAD could pass the version-match check
|
||||
# while still being out of sync with what's on the release branch.
|
||||
echo "Fetching ${UPSTREAM_REMOTE} ${UPSTREAM_BRANCH}..."
|
||||
git fetch --quiet "${UPSTREAM_REMOTE}" "${UPSTREAM_BRANCH}"
|
||||
|
||||
# Catch the race where another maintainer has already published this tag.
|
||||
if git ls-remote --exit-code --tags "${UPSTREAM_REMOTE}" \
|
||||
"refs/tags/${TAG}" >/dev/null 2>&1; then
|
||||
echo "Error: tag ${TAG} already exists on ${UPSTREAM_REMOTE}." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Compare against FETCH_HEAD rather than refs/remotes/<remote>/<branch>:
|
||||
# FETCH_HEAD is always written by `git fetch <remote> <branch>`, while the
|
||||
# remote-tracking ref depends on the user's refspec configuration.
|
||||
HEAD_SHA="$(git rev-parse HEAD)"
|
||||
UP_SHA="$(git rev-parse FETCH_HEAD)"
|
||||
if [[ "${HEAD_SHA}" != "${UP_SHA}" ]]; then
|
||||
AHEAD="$(git rev-list --count FETCH_HEAD..HEAD)"
|
||||
BEHIND="$(git rev-list --count HEAD..FETCH_HEAD)"
|
||||
cat >&2 <<EOF
|
||||
|
||||
Error: HEAD does not match ${UPSTREAM_REMOTE}/${UPSTREAM_BRANCH}.
|
||||
HEAD: ${HEAD_SHA}
|
||||
Upstream: ${UP_SHA}
|
||||
Ahead: ${AHEAD}, behind: ${BEHIND}.
|
||||
|
||||
Push (or wait for) the version bump on ${UPSTREAM_BRANCH}, then re-run.
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Parse version.go from HEAD (now confirmed in sync with upstream) so the
|
||||
# check reflects exactly what the tag will commit to. A single awk pass
|
||||
# extracts all four constants in the order they appear in the file. The
|
||||
# `|| :` keeps `set -e` from terminating the script if AppPreRelease is
|
||||
# absent from version.go and the final read hits EOF.
|
||||
{ read -r M && read -r m && read -r p && read -r pre || :; } < <(
|
||||
git show "HEAD:${VERSION_FILE}" 2>/dev/null | awk '
|
||||
/^[[:space:]]*AppMajor[[:space:]]+uint[[:space:]]*=/ { sub(/.*=[[:space:]]*/,""); sub(/[^0-9].*/,""); print }
|
||||
/^[[:space:]]*AppMinor[[:space:]]+uint[[:space:]]*=/ { sub(/.*=[[:space:]]*/,""); sub(/[^0-9].*/,""); print }
|
||||
/^[[:space:]]*AppPatch[[:space:]]+uint[[:space:]]*=/ { sub(/.*=[[:space:]]*/,""); sub(/[^0-9].*/,""); print }
|
||||
/^[[:space:]]*AppPreRelease[[:space:]]*=/ { match($0,/"[^"]*"/); print substr($0,RSTART+1,RLENGTH-2) }
|
||||
'
|
||||
)
|
||||
|
||||
if [[ -z "${M}" || -z "${m}" || -z "${p}" ]]; then
|
||||
echo "Error: failed to parse version constants from HEAD:${VERSION_FILE}." \
|
||||
>&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Go treats `01` as an octal literal but %d prints it as decimal; force
|
||||
# base-10 here so we match build.Version()'s output.
|
||||
EXPECTED="v$((10#$M)).$((10#$m)).$((10#$p))"
|
||||
[[ -n "${pre}" ]] && EXPECTED="${EXPECTED}-${pre}"
|
||||
|
||||
echo "Requested: ${TAG}"
|
||||
echo "Expected: ${EXPECTED} (from HEAD:${VERSION_FILE})"
|
||||
|
||||
if [[ "${TAG}" != "${EXPECTED}" ]]; then
|
||||
cat >&2 <<EOF
|
||||
|
||||
Error: requested tag does not match HEAD:${VERSION_FILE}.
|
||||
Requested: ${TAG}
|
||||
Expected: ${EXPECTED}
|
||||
|
||||
Bump AppMajor/AppMinor/AppPatch/AppPreRelease and commit before tagging.
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Creating signed tag ${TAG} at ${HEAD_SHA}."
|
||||
git tag -s -m "${TAG}" "${TAG}"
|
||||
echo "Done. Push with: git push ${UPSTREAM_REMOTE} ${TAG}"
|
||||
Loading…
Add table
Add a link
Reference in a new issue