Skip to content
Open
Show file tree
Hide file tree
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
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
28 changes: 28 additions & 0 deletions scripts/release-install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
#
# Installs the release scripts to ~/.local/bin so they can be run
# from anywhere inside the EUI repo without checking out this branch.
#
# Usage: bash <(curl -s https://raw.githubusercontent.com/acstll/eui/feat/release-automation/scripts/release-install.sh)
#

set -euo pipefail

BRANCH="feat/release-automation"
BASE_URL="https://raw.githubusercontent.com/acstll/eui/${BRANCH}/scripts"
Comment on lines +6 to +12
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

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

release-install.sh is hardcoded to download scripts from a personal fork/branch (acstll/eui + feat/release-automation). Once this PR is merged, that URL/branch may not exist and the installer will break. Consider switching to https://raw.githubusercontent.com/elastic/eui/main/scripts (or dynamically deriving the repo/branch via args/env vars) so the install script keeps working long-term.

Suggested change
# Usage: bash <(curl -s https://raw.githubusercontent.com/acstll/eui/feat/release-automation/scripts/release-install.sh)
#
set -euo pipefail
BRANCH="feat/release-automation"
BASE_URL="https://raw.githubusercontent.com/acstll/eui/${BRANCH}/scripts"
# Usage: bash <(curl -s https://raw.githubusercontent.com/elastic/eui/main/scripts/release-install.sh)
#
set -euo pipefail
REPO_OWNER="${REPO_OWNER:-elastic}"
REPO_NAME="${REPO_NAME:-eui}"
BRANCH="${BRANCH:-main}"
BASE_URL="https://raw.githubusercontent.com/${REPO_OWNER}/${REPO_NAME}/${BRANCH}/scripts"

Copilot uses AI. Check for mistakes.
DEST="${HOME}/.local/bin"

SCRIPTS=(release-prep.sh release-publish.sh)

mkdir -p "$DEST"

for script in "${SCRIPTS[@]}"; do
echo "Installing ${script} → ${DEST}/${script}"
curl -fsSL "${BASE_URL}/${script}" -o "${DEST}/${script}"
chmod +x "${DEST}/${script}"
done

echo ""
echo "Done. Make sure ~/.local/bin is in your PATH, then run from the EUI repo:"
echo " release-prep.sh"
echo " release-publish.sh"
135 changes: 135 additions & 0 deletions scripts/release-prep.sh
Original file line number Diff line number Diff line change
@@ -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
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

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

The repo’s CI/release workflows use yarn install --immutable (Yarn 4) to ensure installs don’t modify the lockfile or create unexpected changes. Using plain yarn here can introduce uncommitted changes (e.g. lockfile/install state) and cause the later dirty-worktree prompt to trigger for unrelated reasons. Consider switching this to yarn install --immutable (and any other flags the repo standardizes on) to keep the release branch clean and reproducible.

Suggested change
yarn
yarn install --immutable

Copilot uses AI. Check for mistakes.
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"
138 changes: 138 additions & 0 deletions scripts/release-publish.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
#!/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"

step "1/6" "Updating main branch..."
git checkout main
git pull upstream main
git fetch upstream --tags
Comment on lines +46 to +49
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

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

This script checks out/pulls main and creates a tag, but it doesn't proactively bail if the working tree is dirty. With set -e, git checkout main can fail in a confusing way (or later steps may operate on an unexpected state). Add an explicit git status --porcelain dirty-worktree check (similar to release-prep.sh) before the checkout/pull/tag steps.

Copilot uses AI. Check for mistakes.

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}
"
Comment on lines +85 to +93
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

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

Changelog extraction currently relies on ls -t .../CHANGELOG_*.md | head -1 and then reads the changelog from the current working tree (after pulling main). This can select the wrong changelog file (mtime-based) and can also mismatch the requested MERGE_SHA if a SHA other than current main HEAD is provided. Prefer locating the changelog file that actually contains the v${new_version} header (e.g., search all CHANGELOG_*.md files) and read it from git show ${MERGE_SHA}:<path>; also consider linking to the tag/commit instead of blob/main so notes match the published release.

Copilot uses AI. Check for mistakes.
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}"