Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 149 additions & 8 deletions tooling/perform-release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ set -euo pipefail

# Ask for confirmation before continuing the release
function confirmOrAbort() {
read -p "Do you want to continue? (y/N): " -n 1 -r
local prompt="${1:-Do you want to continue? (y/N): }"
local reply
read -p "$prompt" -n 1 -r reply
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
if [[ ! $reply =~ ^[Yy]$ ]]; then
echo "Aborting."
exit 1
fi
Expand Down Expand Up @@ -48,7 +50,7 @@ if ! git diff-index --quiet HEAD; then
fi

# Check if clone is up to date
if ! git fetch "$REMOTE" --quiet ; then
if ! git fetch "$REMOTE" --tags --quiet ; then
echo "❌ Unable to fetch the latest changes from $REMOTE. Please check your network connection."
exit 1
fi
Expand All @@ -58,6 +60,41 @@ if ! git diff-index --quiet "$REMOTE/$CURRENT_BRANCH"; then
fi
echo "✅ Working copy is clean and up-to-date."

# Check if gh CLI and jq are available
if ! command -v gh &>/dev/null; then
echo "❌ GitHub CLI (gh) is not installed or not in PATH. Please install it to proceed."
exit 1
fi
if ! command -v jq &>/dev/null; then
echo "❌ jq is not installed or not in PATH. Please install it to proceed."
exit 1
fi

# Check gh authentication
if ! gh auth status &>/dev/null; then
echo "❌ GitHub CLI is not authenticated. Please run 'gh auth login' first."
exit 1
fi

# Check that the current user is a member of the release owner team
CURRENT_USER=$(gh api user --jq '.login' 2>/dev/null || true)
if [ -z "$CURRENT_USER" ]; then
echo "❌ Failed to determine GitHub username. Check your network connection and token scopes."
exit 1
fi
if ! MEMBERSHIP_STATE=$(gh api "orgs/DataDog/teams/dd-trace-java-releasers/memberships/$CURRENT_USER" \
--jq '.state'); then
echo "❌ You ($CURRENT_USER) are not an active member of the dd-trace-java-releasers release owner team."
echo " Only release owners are allowed to perform a release."
exit 1
fi
if [ "$MEMBERSHIP_STATE" != "active" ]; then
echo "❌ Your membership in the dd-trace-java-releasers team is not active (state: $MEMBERSHIP_STATE)."
echo " Only active release owners are allowed to perform a release."
exit 1
fi
echo "✅ GitHub CLI authenticated as release owner $CURRENT_USER."

# Check the git log history
LAST_RELEASE_TAG=$(git describe --tags --abbrev=0 --match='v[0-9]*.[0-9]*.[0-9]*')
echo "ℹ️ Last release version: $LAST_RELEASE_TAG"
Expand All @@ -72,21 +109,125 @@ else
fi

# Get the next release version
VERSION=$(echo "$LAST_RELEASE_TAG" | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sed 's/^v//' || true)
if [ -z "$VERSION" ]; then
VERSION="${LAST_RELEASE_TAG#v}"
if [ -z "$VERSION" ] || ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "❌ Unable to determine the next release version from the last release tag: $LAST_RELEASE_TAG"
exit 1
fi
IFS=. read -r _major _minor _patch <<< "$VERSION"
if [ "$MINOR_RELEASE" = true ]; then
NEXT_RELEASE_VERSION=$(echo "$VERSION" | awk -F. '{printf "v%d.%d.0", $1, $2 + 1}')
NEXT_RELEASE_VERSION="v${_major}.$(( _minor + 1 )).0"
else
NEXT_RELEASE_VERSION=$(echo "$VERSION" | awk -F. '{printf "v%d.%d.%d", $1, $2, $3 + 1}')
NEXT_RELEASE_VERSION="v${_major}.${_minor}.$(( _patch + 1 ))"
fi
echo "ℹ️ Next release version: $NEXT_RELEASE_VERSION"

# Check the release tag does not already exist locally or remotely.
# The tag can exist if it was created on a branch not reachable from HEAD (e.g. a failed release on a different branch)
# or if a prior release attempt was interrupted before pushing. After `git fetch --tags` above, remote tags are
# included in this check.
if git rev-parse "refs/tags/$NEXT_RELEASE_VERSION" &>/dev/null; then
echo "❌ Tag '$NEXT_RELEASE_VERSION' already exists. Has this release already been tagged?"
exit 1
fi
echo "✅ Release tag '$NEXT_RELEASE_VERSION' does not yet exist."

# Check that an open GitHub milestone exists for the next release version
MILESTONE_TITLE="${NEXT_RELEASE_VERSION#v}"
if ! MILESTONE_NUMBERS=$(gh api --paginate "repos/{owner}/{repo}/milestones?state=open&per_page=100" \
--jq ".[] | select(.title == \"$MILESTONE_TITLE\") | .number"); then
echo "❌ Failed to query GitHub milestones. Check your network connection."
exit 1
fi
if [ -z "$MILESTONE_NUMBERS" ]; then
echo "❌ No open GitHub milestone found for version '$MILESTONE_TITLE'."
echo " Please create the milestone and assign PRs to it before performing a release."
exit 1
fi
MILESTONE_COUNT=$(printf '%s\n' "$MILESTONE_NUMBERS" | grep -c .)
if [ "$MILESTONE_COUNT" -gt 1 ]; then
echo "❌ Multiple open milestones found for version '$MILESTONE_TITLE' (numbers: $(printf '%s ' "$MILESTONE_NUMBERS"))."
echo " Please resolve the duplicate milestones before performing a release."
exit 1
fi
MILESTONE_NUMBER="$MILESTONE_NUMBERS"
echo "✅ GitHub milestone '$MILESTONE_TITLE' found (milestone #$MILESTONE_NUMBER)."

# Check that the milestone has no open PRs (open issues are expected and allowed).
# Use `jq -s 'add | length'` to sum counts across all pages returned by --paginate.
if ! OPEN_PRS=$(gh api --paginate \
"repos/{owner}/{repo}/issues?milestone=$MILESTONE_NUMBER&state=open&per_page=100" \
--jq '[.[] | select(.pull_request != null)]' | jq -s 'add | length'); then
echo "❌ Failed to query milestone '$MILESTONE_TITLE'. Check your network connection."
exit 1
fi
if [ "$OPEN_PRS" -gt 0 ]; then
echo "❌ Milestone '$MILESTONE_TITLE' still has $OPEN_PRS open PR(s):"
gh api --paginate "repos/{owner}/{repo}/issues?milestone=$MILESTONE_NUMBER&state=open&per_page=100" \
--jq '.[] | select(.pull_request != null) | " #\(.number) \(.title)"' || true
echo " All PRs must be merged before tagging a release."
exit 1
fi
echo "✅ All PRs in milestone '$MILESTONE_TITLE' are merged."

# Check that all closed PRs in the milestone carry the required labels.
# This is a release-time defense-in-depth check: even if CI enforces labels at PR submission,
# PRs merged before that enforcement (or via merge queues with bypassed checks) may be missing them.
# Required: (comp:* or inst:*) AND (type:*), OR 'tag: no release notes'.
Comment on lines +173 to +176
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❔ question: ‏What's the point? It's a duplicate of the PR checks workflow.

if ! NONCOMPLIANT=$(gh api --paginate \
"repos/{owner}/{repo}/issues?milestone=$MILESTONE_NUMBER&state=closed&per_page=100" \
--jq '[.[] | select(.pull_request != null) |
(.labels // [] | map(.name)) as $l |
select(
($l | any(. == "tag: no release notes") | not) and
(
($l | any(startswith("comp:") or startswith("inst:")) | not) or
($l | any(startswith("type:")) | not)
)
) | " #\(.number) \(.title)"] | .[]'); then
echo "❌ Failed to query milestone PRs. Check your network connection."
exit 1
fi
if [ -n "$NONCOMPLIANT" ]; then
echo "⚠️ The following PRs in milestone '$MILESTONE_TITLE' are missing required labels:"
echo "$NONCOMPLIANT"
echo " Each PR needs (a 'comp:' or 'inst:' label) AND (a 'type:' label), or 'tag: no release notes'."
confirmOrAbort "Continue despite missing labels? (y/N): "
else
echo "✅ All PRs in milestone '$MILESTONE_TITLE' have required labels."
fi

# Check GPG signing key is configured and usable for signing (release tags are signed with -s).
# Use a test-sign to catch expired or revoked keys before the point of no return.
SIGNING_KEY=$(git config --get user.signingkey 2>/dev/null || git config --get user.email 2>/dev/null || true)
if [ -z "$SIGNING_KEY" ]; then
echo "❌ No GPG signing key configured. Release tags must be signed."
echo " Configure one with: git config user.signingkey <KEY_ID>"
exit 1
fi
if ! echo "test" | gpg --no-tty --sign --local-user "$SIGNING_KEY" --output /dev/null 2>/dev/null; then
echo "❌ GPG signing key '$SIGNING_KEY' cannot be used for signing (missing, expired, revoked, or passphrase required non-interactively)."
echo " Please verify the key is available and valid: gpg --list-secret-keys '$SIGNING_KEY'"
exit 1
fi
echo "✅ GPG signing key is configured and usable."

# For minor releases: require explicit acknowledgment of manual pre-cut verification steps
if [ "$MINOR_RELEASE" = true ]; then
echo ""
echo "ℹ️ Minor release — manual pre-cut verification required (Steps 1–2 of the release process)."
confirmOrAbort "Step 1: Have you reviewed the APM Performance SDK SLO dashboard and found no regressions? (y/N): "
confirmOrAbort "Step 2: Have you reviewed the Test Optimization Performance Dashboard and found no increased overhead? (y/N): "
echo ""
fi

# Create and push the release tag
echo "ℹ️ The release tag will be created and pushed. No abort is possible after this point."
confirmOrAbort
git tag -a -s -m "Release $NEXT_RELEASE_VERSION" "$NEXT_RELEASE_VERSION"
git push "$REMOTE" "$NEXT_RELEASE_VERSION" --no-verify
if ! git push "$REMOTE" "$NEXT_RELEASE_VERSION" --no-verify; then
echo "❌ Push failed. Deleting the local tag to allow a clean retry."
git tag -d "$NEXT_RELEASE_VERSION"
exit 1
fi
echo "✅ Release tag $NEXT_RELEASE_VERSION created and pushed to $REMOTE."
Loading