diff --git a/package.json b/package.json index c5bc5d81aa92..b97095f89986 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,8 @@ "build": "printf $'\\x1b[K\\x1b[37;41mPlease run this script from the \\x1b[1;4mpackages/eui\\x1b[0m\\x1b[37;41m directory instead\\x1b[0m\\n'; exit 1", "watch": "node scripts/watch-eui.js", "release": "node scripts/release", + "release:prep": "bash scripts/release-prep.sh", + "release:publish": "bash scripts/release-publish.sh", "clean": "node scripts/clean.mjs" }, "repository": { diff --git a/scripts/release-prep.sh b/scripts/release-prep.sh new file mode 100755 index 000000000000..3dd3c270d79c --- /dev/null +++ b/scripts/release-prep.sh @@ -0,0 +1,135 @@ +#!/usr/bin/env bash +# +# Official release preparation script (pre-PR) +# +# Automates initial steps of the process: +# - Log out of npm +# - Checkout main, pull latest from upstream +# - Create a timestamped release branch +# - Build the release CLI +# - Run the release dry-run (interactive) +# - (User confirms in the interactive CLI) +# - Push the branch to origin and open a PR +# +# Usage: yarn release:prep +# + +set -euo pipefail + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BOLD='\033[1m' +RESET='\033[0m' + +step() { + echo "" + echo -e "${GREEN}${BOLD}[$1]${RESET} $2" +} + +warn() { + echo -e "${YELLOW}Warning:${RESET} $1" +} + +error() { + echo -e "${RED}Error:${RESET} $1" >&2 + exit 1 +} + +REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) || error "Not inside a git repository" +cd "$REPO_ROOT" + +PACKAGE_DIRS=(packages/eui packages/eui-theme-common packages/eui-theme-borealis packages/docusaurus-preset packages/docusaurus-theme packages/eslint-plugin) + +git remote get-url upstream &>/dev/null || error "'upstream' remote not found. Please add it: git remote add upstream git@github.com:elastic/eui.git" + +# Bail early if there are uncommitted changes +if [[ -n "$(git status --porcelain)" ]]; then + error "Working tree is dirty. Please commit or stash your changes before running this script." +fi + +step "1/8" "Ensuring npm is not authenticated..." +npm logout 2>/dev/null || true +yarn npm logout 2>/dev/null || true + +step "2/8" "Checking out main branch..." +git checkout main + +step "3/8" "Pulling latest changes from upstream..." +git pull upstream main + +BRANCH_NAME="release/$(date +%s)" +step "4/8" "Creating release branch: ${BOLD}${BRANCH_NAME}${RESET}" +git checkout -b "$BRANCH_NAME" + +step "5/8" "Installing dependencies and building release CLI..." +yarn +yarn workspace @elastic/eui-release-cli run build + +step "6/8" "Starting release process (dry-run)..." +echo "" +yarn release run official --dry-run --allow-custom --skip-auth-check --use-auth-token + +# Check for uncommitted changes after the release CLI run +if [[ -n "$(git status --porcelain)" ]]; then + echo "" + warn "There are uncommitted changes in the working tree:" + git status --short + echo "" + read -r -p "Continue pushing without these changes? They won't be included in the PR. (y/N) " confirm + if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then + echo "" + echo "Stopped. Commit your changes and re-run from step 7 manually:" + echo " git push -u origin $BRANCH_NAME" + exit 0 + fi +fi + +step "7/8" "Pushing branch to origin..." +git push -u origin "$BRANCH_NAME" + +step "8/8" "Opening release PR..." + +PR_TITLE_PARTS="" +PR_BODY_LINES="" + +for pkg_dir in "${PACKAGE_DIRS[@]}"; do + pkg_json="${pkg_dir}/package.json" + [[ -f "$pkg_json" ]] || continue + + new_version=$(node -p "require('./${pkg_json}').version") + old_version=$(git show "main:${pkg_json}" 2>/dev/null | node -p "JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')).version" 2>/dev/null || echo "") + + if [[ -n "$old_version" && "$new_version" != "$old_version" ]]; then + pkg_name=$(node -p "require('./${pkg_json}').name") + + if [[ -n "$PR_TITLE_PARTS" ]]; then + PR_TITLE_PARTS="${PR_TITLE_PARTS}, ${pkg_name} v${new_version}" + else + PR_TITLE_PARTS="${pkg_name} v${new_version}" + fi + PR_BODY_LINES="${PR_BODY_LINES}\n- \`${pkg_name}\` - v${old_version} → v${new_version}" + fi +done + +if [[ -z "$PR_TITLE_PARTS" ]]; then + error "No changed package versions detected. Did the release dry-run update any versions?" +fi + +PR_TITLE="Release: ${PR_TITLE_PARTS}" +PR_BODY="$(printf "Packages to release:\n${PR_BODY_LINES}")" + +PR_URL=$(gh pr create \ + --title "$PR_TITLE" \ + --body "$PR_BODY" \ + --label "skip-changelog" \ + --label "release" \ + --base main) + +echo "" +echo -e "${GREEN}${BOLD}Prep complete!${RESET}" +echo "" +echo -e " PR: ${BOLD}${PR_URL}${RESET}" +echo "" +echo -e " ${BOLD}After the PR is merged:${RESET}" +echo -e " yarn release:publish" diff --git a/scripts/release-publish.sh b/scripts/release-publish.sh new file mode 100755 index 000000000000..76ccdae798a1 --- /dev/null +++ b/scripts/release-publish.sh @@ -0,0 +1,142 @@ +#!/usr/bin/env bash +# +# Official release publish script (post-merge) +# +# Automates final steps of the process: +# - Detects the EUI version and changed workspaces +# - Tags the merge commit +# - Pushes the tag to upstream +# - Triggers the GitHub Actions release workflow +# - Creates a GitHub release +# +# Usage: yarn release:publish [merge-commit-sha] +# +# If no SHA is provided, defaults to HEAD on main. +# + +set -euo pipefail + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BOLD='\033[1m' +RESET='\033[0m' + +step() { + echo "" + echo -e "${GREEN}${BOLD}[$1]${RESET} $2" +} + +warn() { + echo -e "${YELLOW}Warning:${RESET} $1" +} + +error() { + echo -e "${RED}Error:${RESET} $1" >&2 + exit 1 +} + +REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) || error "Not inside a git repository" +cd "$REPO_ROOT" + +PACKAGE_DIRS=(packages/eui packages/eui-theme-common packages/eui-theme-borealis packages/docusaurus-preset packages/docusaurus-theme packages/eslint-plugin) + +git remote get-url upstream &>/dev/null || error "'upstream' remote not found" + +if [[ -n "$(git status --porcelain)" ]]; then + error "Working tree is dirty. Please commit or stash your changes before running this script." +fi + +step "1/6" "Updating main branch..." +git checkout main +git pull upstream main +git fetch upstream --tags + +MERGE_SHA="${1:-$(git rev-parse HEAD)}" + +step "2/6" "Detecting release details from ${BOLD}${MERGE_SHA:0:12}${RESET}..." + +EUI_VERSION=$(git show "${MERGE_SHA}:packages/eui/package.json" | node -p "JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')).version") +TAG_NAME="v${EUI_VERSION}" + +if git rev-parse "$TAG_NAME" &>/dev/null; then + error "Tag ${TAG_NAME} already exists. Has this release already been published?" +fi + +PREV_TAG=$(git describe --tags --abbrev=0 "${MERGE_SHA}^" 2>/dev/null) || error "Could not find a previous release tag" + +# Detect changed workspaces and collect changelog entries for the GitHub release +CHANGED_WORKSPACES="" +RELEASE_BODY="" + +for pkg_dir in "${PACKAGE_DIRS[@]}"; do + pkg_json="${pkg_dir}/package.json" + + git show "${MERGE_SHA}:${pkg_json}" &>/dev/null || continue + + new_version=$(git show "${MERGE_SHA}:${pkg_json}" | node -p "JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')).version") + old_version=$(git show "${PREV_TAG}:${pkg_json}" 2>/dev/null | node -p "JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')).version" 2>/dev/null || echo "") + + if [[ "$new_version" != "$old_version" ]]; then + pkg_name=$(git show "${MERGE_SHA}:${pkg_json}" | node -p "JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')).name") + + if [[ -n "$CHANGED_WORKSPACES" ]]; then + CHANGED_WORKSPACES="${CHANGED_WORKSPACES},${pkg_name}" + else + CHANGED_WORKSPACES="${pkg_name}" + fi + + # Extract changelog section for this version + changelog_file=$(ls -t "${pkg_dir}"/changelogs/CHANGELOG_*.md 2>/dev/null | head -1) + if [[ -n "$changelog_file" ]]; then + escaped_version="${new_version//./\\.}" + changelog_section=$(awk "/^## \[\`v${escaped_version}\`\]/{found=1; next} /^## \[/{if(found) exit} found" "$changelog_file") + if [[ -n "$changelog_section" ]]; then + RELEASE_BODY="${RELEASE_BODY}### \`${pkg_name}\` [v${new_version}](https://github.com/elastic/eui/blob/main/${changelog_file}) +${changelog_section} +" + fi + fi + fi +done + +if [[ -z "$CHANGED_WORKSPACES" ]]; then + error "No changed workspaces detected. Are you sure the release PR was merged?" +fi + +step "3/6" "Release summary" +echo "" +echo -e " Tag: ${BOLD}${TAG_NAME}${RESET}" +echo -e " Commit: ${BOLD}${MERGE_SHA:0:12}${RESET}" +echo -e " Previous tag: ${BOLD}${PREV_TAG}${RESET}" +echo -e " Workspaces: ${BOLD}${CHANGED_WORKSPACES}${RESET}" +echo "" +read -r -p "Proceed with tagging and triggering the release? (y/N) " confirm +if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then + echo "Aborted." + exit 0 +fi + +step "4/6" "Creating and pushing tag ${BOLD}${TAG_NAME}${RESET}..." +git tag -a "$TAG_NAME" "$MERGE_SHA" -m "@elastic/eui ${TAG_NAME}" +git push upstream "$TAG_NAME" --no-verify + +step "5/6" "Triggering release workflow..." +gh workflow run release.yml \ + --repo elastic/eui \ + -f release_ref="$MERGE_SHA" \ + -f type=official \ + -f workspaces="$CHANGED_WORKSPACES" \ + -f dry_run=false + +step "6/6" "Creating GitHub release..." +gh release create "$TAG_NAME" \ + --repo elastic/eui \ + --title "$TAG_NAME" \ + --notes "$RELEASE_BODY" + +echo "" +echo -e "${GREEN}${BOLD}Release triggered!${RESET}" +echo "" +echo -e " Release: ${BOLD}https://github.com/elastic/eui/releases/tag/${TAG_NAME}${RESET}" +echo -e " Workflow: ${BOLD}https://github.com/elastic/eui/actions/workflows/release.yml${RESET}"