From 824a9cd36c8d9c57d7d956a25af1f06c1b76f834 Mon Sep 17 00:00:00 2001 From: Paul Wackerow <54227730+wackerow@users.noreply.github.com> Date: Wed, 7 Jan 2026 16:18:27 -0800 Subject: [PATCH 1/5] feat: add prepare-release command --- .claude/commands/prepare-release.md | 148 ++++++++++++++++++ src/scripts/prepare-release.sh | 234 ++++++++++++++++++++++++++++ 2 files changed, 382 insertions(+) create mode 100644 .claude/commands/prepare-release.md create mode 100755 src/scripts/prepare-release.sh diff --git a/.claude/commands/prepare-release.md b/.claude/commands/prepare-release.md new file mode 100644 index 00000000000..a85d6a7c8a6 --- /dev/null +++ b/.claude/commands/prepare-release.md @@ -0,0 +1,148 @@ +--- +description: Prepare a release - version bump, branch sync, release notes cleanup, and deploy PR +allowed-tools: Bash, Read, Write, AskUserQuestion +argument-hints: --major|--minor|--patch +--- + +# Prepare Release Command + +Automates the ethereum.org deployment workflow using `src/scripts/prepare-release.sh` for deterministic operations and Claude for intelligent tasks (version suggestion, release note cleanup). + +## Arguments + +Details for $ARGUMENTS + +- `--major` - Major release (breaking/stack changes) +- `--minor` - Minor release (new features, content, translations) +- `--patch` - Patch release (bug fixes, typos, small updates) +- _(no flag)_ - Analyze changes and suggest version type + +## Execution Flow + +### Step 1: Pre-flight Checks + +Run the script to verify environment and sync branches: + +```bash +./src/scripts/prepare-release.sh preflight +``` + +This handles: verify on `dev` branch, clean working tree, `gh` authenticated, back-merge `master` → `staging` → `dev`, pull latest. + +If this fails, stop and report the error. + +### Step 2: Determine Version Type + +**If flag provided** (`--major`, `--minor`, `--patch`): +Extract from `$ARGUMENTS` and proceed to Step 3. + +**If no flag provided**: +1. Fetch draft release: `./src/scripts/prepare-release.sh fetch-draft` +2. Analyze the changes: + - **Major**: Stack/framework changes, significant breaking updates (rare) + - **Minor**: New features, new content pages, significant translations, new components + - **Patch**: Bug fixes, typo corrections, small content updates, dependency bumps +3. Provide a ONE-LINE suggestion with reasoning +4. Use `AskUserQuestion` to confirm: "Proceed with **X** release?" with options: Yes / Change to major / Change to minor / Change to patch +5. Proceed only after confirmation + +### Step 3: Version Bump + +```bash +VERSION=$(./src/scripts/prepare-release.sh version ) +``` + +### Step 4: Merge to Staging + +```bash +./src/scripts/prepare-release.sh merge-staging +``` + +### Step 5: Fetch Draft Release + +```bash +DRAFT_JSON=$(./src/scripts/prepare-release.sh fetch-draft) +``` + +Parse the JSON to extract `tagName` (DRAFT_TAG) and `body`. If no draft exists, error out. + +### Step 6: Clean Release Notes + +The draft release body needs cleanup. Apply these filters: + +**Remove from CHANGES sections** (lines matching these patterns): +- Author is `allcontributors` or `allcontributors[bot]` (these are just additions to our all-contributors list, not pertinent to actual changes) +- PR title contains "Release candidate v" (release management) +- PR title contains "Deploy v" (release management) +- PR title starts with "Staging -> dev" or "Staging -> Dev" (back-merge) +- PR title starts with "Master -> staging" or "Master -> Staging" (back-merge) +- PR title starts with "Back merge" (back-merge) +- PR title is just a version number like "v10.20.0" or "v11.0.0" (version bump commits) +- PR title starts with "Update translation contributors from Crowdin" (automated Crowdin) +- PR title starts with "Update translation progress from Crowdin" (automated Crowdin) + +Note: Keep entries from other bots like `dependabot`, `claude[bot]`, `github-actions` - their PRs ARE meaningful changes (dependency updates, code changes, etc). We just don't thank them as human contributors. + +**Remove from CONTRIBUTORS section** (the "Thank you @..." line): +These accounts should be filtered from the contributors list: +- `dependabot` +- `dependabot[bot]` +- `allcontributors` +- `allcontributors[bot]` +- `claude` +- `claude[bot]` +- `github-actions` +- `github-actions[bot]` +- `actions-user` + +**Keep the structure intact**: +- Keep section headers (⚡️ Changes, 🌐 Translations, 🐛 Bug Fix, etc.) +- Keep the `***` separators +- Keep the 🦄 Contributors section (just filter the bot names from it) +- Remove empty sections if all entries were filtered out + +Write cleaned body to temp file: +```bash +# Write the cleaned release notes to a temp file +cat << 'EOF' > /tmp/claude/release-notes.md + +EOF +``` + +### Step 7: Publish Release + +```bash +RELEASE_URL=$(./src/scripts/prepare-release.sh publish "$VERSION" "$DRAFT_TAG" /tmp/claude/release-notes.md) +``` + +### Step 8: Create Deploy PR + +```bash +PR_URL=$(./src/scripts/prepare-release.sh create-pr "$VERSION" /tmp/claude/release-notes.md) +``` + +### Step 9: Report Success + +Output summary: +``` +✅ Release prepared successfully! + +Version: vX.X.X +Release: +Deploy PR: + +Next step: Review the preview build, then merge the PR when ready. +``` + +## Error Handling + +- If any git operation fails, stop and report the error +- If `gh` commands fail, check authentication and permissions +- If no draft release exists, error with clear message +- If merge conflicts occur during back-merge, stop and instruct user to resolve manually + +## Notes + +- This command does NOT merge the deploy PR - that remains a manual step after QA +- The release can be edited after publishing if corrections are needed +- Always verify the cleaned release notes look correct before the PR is merged diff --git a/src/scripts/prepare-release.sh b/src/scripts/prepare-release.sh new file mode 100755 index 00000000000..2d57252f7f2 --- /dev/null +++ b/src/scripts/prepare-release.sh @@ -0,0 +1,234 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Prepare Release Script +# Handles deterministic git/gh operations for ethereum.org releases +# +# Usage: +# ./src/scripts/prepare-release.sh preflight # Run pre-flight checks and back-merge sync +# ./src/scripts/prepare-release.sh version # Bump version (major|minor|patch) and push +# ./src/scripts/prepare-release.sh merge-staging # Merge dev into staging +# ./src/scripts/prepare-release.sh fetch-draft # Fetch draft release body +# ./src/scripts/prepare-release.sh publish # Publish release +# ./src/scripts/prepare-release.sh create-pr # Create deploy PR + +REPO="ethereum/ethereum-org-website" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +log_error() { echo -e "${RED}[ERROR]${NC} $1"; } + +cmd_preflight() { + log_info "Running pre-flight checks..." + + # Check current branch + CURRENT_BRANCH=$(git branch --show-current) + if [[ "$CURRENT_BRANCH" != "dev" ]]; then + log_error "Must be on 'dev' branch. Currently on: $CURRENT_BRANCH" + exit 1 + fi + log_info "✓ On dev branch" + + # Check clean working tree + if [[ -n $(git status --porcelain) ]]; then + log_error "Working tree is not clean. Commit or stash changes first." + exit 1 + fi + log_info "✓ Working tree clean" + + # Check gh auth + if ! gh auth status &>/dev/null; then + log_error "GitHub CLI not authenticated. Run 'gh auth login' first." + exit 1 + fi + log_info "✓ GitHub CLI authenticated" + + # Fetch latest from origin + log_info "Fetching latest from origin..." + git fetch origin + + # Back-merge: master -> staging (if needed) + log_info "Checking if master needs to be merged into staging..." + MASTER_AHEAD=$(git rev-list --count origin/staging..origin/master) + if [[ "$MASTER_AHEAD" -gt 0 ]]; then + log_info "Merging origin/master into staging ($MASTER_AHEAD commits)..." + git checkout staging + git merge origin/master -m "Merge master into staging" + git push origin staging + git checkout dev + else + log_info "✓ staging is up to date with master" + fi + + # Back-merge: staging -> dev (if needed) + log_info "Checking if staging needs to be merged into dev..." + STAGING_AHEAD=$(git rev-list --count origin/dev..origin/staging) + if [[ "$STAGING_AHEAD" -gt 0 ]]; then + log_info "Merging origin/staging into dev ($STAGING_AHEAD commits)..." + git merge origin/staging -m "Merge staging into dev" + git push origin dev + else + log_info "✓ dev is up to date with staging" + fi + + # Pull latest dev + log_info "Pulling latest origin/dev..." + git pull origin dev + + log_info "✓ Pre-flight checks complete" +} + +cmd_version() { + local VERSION_TYPE="${1:-}" + + if [[ -z "$VERSION_TYPE" ]]; then + log_error "Version type required: major, minor, or patch" + exit 1 + fi + + if [[ ! "$VERSION_TYPE" =~ ^(major|minor|patch)$ ]]; then + log_error "Invalid version type: $VERSION_TYPE. Must be major, minor, or patch" + exit 1 + fi + + log_info "Bumping $VERSION_TYPE version..." + pnpm version "$VERSION_TYPE" + + NEW_VERSION=$(node -p "require('./package.json').version") + log_info "New version: v$NEW_VERSION" + + log_info "Pushing to origin with tags..." + git push origin dev --follow-tags + + echo "$NEW_VERSION" +} + +cmd_merge_staging() { + log_info "Merging dev into staging..." + + git checkout staging + git merge dev -m "Merge dev into staging for release" + git push origin staging + git checkout dev + + log_info "✓ dev merged into staging" +} + +cmd_fetch_draft() { + log_info "Fetching draft release..." + + # Get all releases and find the draft one + DRAFT_RELEASE=$(gh release list --repo "$REPO" --json tagName,isDraft,body,name --limit 10 | \ + node -e " + const data = JSON.parse(require('fs').readFileSync(0, 'utf8')); + const draft = data.find(r => r.isDraft); + if (!draft) { + console.error('No draft release found'); + process.exit(1); + } + console.log(JSON.stringify(draft)); + ") + + if [[ -z "$DRAFT_RELEASE" ]]; then + log_error "No draft release found. Ensure Release Drafter workflow has run." + exit 1 + fi + + echo "$DRAFT_RELEASE" +} + +cmd_publish() { + local VERSION="${1:-}" + local DRAFT_TAG="${2:-}" + local BODY_FILE="${3:-}" + + if [[ -z "$VERSION" || -z "$DRAFT_TAG" || -z "$BODY_FILE" ]]; then + log_error "Usage: prepare-release.sh publish " + exit 1 + fi + + if [[ ! -f "$BODY_FILE" ]]; then + log_error "Body file not found: $BODY_FILE" + exit 1 + fi + + log_info "Publishing release v$VERSION..." + + gh release edit "$DRAFT_TAG" \ + --repo "$REPO" \ + --tag "v$VERSION" \ + --title "v$VERSION" \ + --notes-file "$BODY_FILE" \ + --draft=false \ + --latest + + log_info "✓ Release v$VERSION published" + echo "https://github.com/$REPO/releases/tag/v$VERSION" +} + +cmd_create_pr() { + local VERSION="${1:-}" + local BODY_FILE="${2:-}" + + if [[ -z "$VERSION" || -z "$BODY_FILE" ]]; then + log_error "Usage: prepare-release.sh create-pr " + exit 1 + fi + + if [[ ! -f "$BODY_FILE" ]]; then + log_error "Body file not found: $BODY_FILE" + exit 1 + fi + + log_info "Creating deploy PR for v$VERSION..." + + PR_URL=$(gh pr create \ + --repo "$REPO" \ + --base master \ + --head staging \ + --title "Deploy v$VERSION" \ + --body-file "$BODY_FILE") + + log_info "✓ Deploy PR created" + echo "$PR_URL" +} + +# Main command router +case "${1:-}" in + preflight) + cmd_preflight + ;; + version) + cmd_version "${2:-}" + ;; + merge-staging) + cmd_merge_staging + ;; + fetch-draft) + cmd_fetch_draft + ;; + publish) + cmd_publish "${2:-}" "${3:-}" "${4:-}" + ;; + create-pr) + cmd_create_pr "${2:-}" "${3:-}" + ;; + *) + echo "Usage: $0 [args]" + echo "" + echo "Commands:" + echo " preflight Run pre-flight checks and back-merge sync" + echo " version Bump version (major|minor|patch) and push" + echo " merge-staging Merge dev into staging" + echo " fetch-draft Fetch draft release body (JSON)" + echo " publish Publish release" + echo " create-pr Create deploy PR" + exit 1 + ;; +esac From 538d496f23f487e2ad153e6ac8b77e6b8bd322ee Mon Sep 17 00:00:00 2001 From: Paul Wackerow <54227730+wackerow@users.noreply.github.com> Date: Wed, 7 Jan 2026 16:32:34 -0800 Subject: [PATCH 2/5] feat: enable execution outside of dev via git worktree --- .claude/commands/prepare-release.md | 14 ++- src/scripts/prepare-release.sh | 144 ++++++++++++++++++++++------ 2 files changed, 126 insertions(+), 32 deletions(-) diff --git a/.claude/commands/prepare-release.md b/.claude/commands/prepare-release.md index a85d6a7c8a6..ac5ee4a3298 100644 --- a/.claude/commands/prepare-release.md +++ b/.claude/commands/prepare-release.md @@ -27,7 +27,9 @@ Run the script to verify environment and sync branches: ./src/scripts/prepare-release.sh preflight ``` -This handles: verify on `dev` branch, clean working tree, `gh` authenticated, back-merge `master` → `staging` → `dev`, pull latest. +This handles: `gh` authenticated, create worktree if not on `dev`, clean working tree, back-merge `master` → `staging` → `dev`, pull latest. + +**Note**: The script can run from any branch. If not on `dev`, it creates a worktree at `../worktrees/ethereum-org-dev` and performs all operations there. If this fails, stop and report the error. @@ -121,7 +123,15 @@ RELEASE_URL=$(./src/scripts/prepare-release.sh publish "$VERSION" "$DRAFT_TAG" / PR_URL=$(./src/scripts/prepare-release.sh create-pr "$VERSION" /tmp/claude/release-notes.md) ``` -### Step 9: Report Success +### Step 9: Cleanup Worktree + +If a worktree was created, clean it up: + +```bash +./src/scripts/prepare-release.sh cleanup +``` + +### Step 10: Report Success Output summary: ``` diff --git a/src/scripts/prepare-release.sh b/src/scripts/prepare-release.sh index 2d57252f7f2..025f436fbfb 100755 --- a/src/scripts/prepare-release.sh +++ b/src/scripts/prepare-release.sh @@ -11,9 +11,18 @@ set -euo pipefail # ./src/scripts/prepare-release.sh fetch-draft # Fetch draft release body # ./src/scripts/prepare-release.sh publish # Publish release # ./src/scripts/prepare-release.sh create-pr # Create deploy PR +# ./src/scripts/prepare-release.sh cleanup # Remove worktree if created REPO="ethereum/ethereum-org-website" +# Worktree configuration +REPO_ROOT=$(git rev-parse --show-toplevel) +WORKTREE_BASE="${PREPARE_RELEASE_WORKTREE_BASE:-/tmp/claude/worktrees}" +WORKTREE_DIR="${WORKTREE_BASE}/ethereum-org-dev" +WORKTREE_MARKER="/tmp/claude/prepare-release-worktree" +USING_WORKTREE=false +WORK_DIR="" + # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' @@ -24,64 +33,129 @@ log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } log_error() { echo -e "${RED}[ERROR]${NC} $1"; } -cmd_preflight() { - log_info "Running pre-flight checks..." - - # Check current branch +# Setup worktree for dev branch if not already on dev +setup_worktree() { CURRENT_BRANCH=$(git branch --show-current) - if [[ "$CURRENT_BRANCH" != "dev" ]]; then - log_error "Must be on 'dev' branch. Currently on: $CURRENT_BRANCH" - exit 1 + + if [[ "$CURRENT_BRANCH" == "dev" ]]; then + WORK_DIR="$REPO_ROOT" + log_info "✓ Already on dev branch" + return 0 fi - log_info "✓ On dev branch" - # Check clean working tree - if [[ -n $(git status --porcelain) ]]; then - log_error "Working tree is not clean. Commit or stash changes first." + log_info "Not on dev branch (on: $CURRENT_BRANCH). Setting up worktree..." + + # Create worktree base directory if needed + mkdir -p "$WORKTREE_BASE" + + # Check if worktree already exists + if [[ -d "$WORKTREE_DIR" ]]; then + # Verify it's a valid worktree + if git worktree list | grep -q "$WORKTREE_DIR"; then + log_info "Using existing worktree at $WORKTREE_DIR" + else + # Directory exists but isn't a worktree - clean it up + log_warn "Cleaning up stale worktree directory..." + rm -rf "$WORKTREE_DIR" + git worktree add "$WORKTREE_DIR" dev + log_info "Created worktree at $WORKTREE_DIR" + fi + else + git worktree add "$WORKTREE_DIR" dev + log_info "Created worktree at $WORKTREE_DIR" + fi + + WORK_DIR="$WORKTREE_DIR" + USING_WORKTREE=true + + # Store marker so cleanup knows worktree was created this session + mkdir -p /tmp/claude + echo "$WORKTREE_DIR" > "$WORKTREE_MARKER" + + log_info "✓ Worktree ready at $WORKTREE_DIR" +} + +# Run a command in the work directory (worktree or repo root) +run_in_workdir() { + if [[ -z "$WORK_DIR" ]]; then + log_error "WORK_DIR not set. Run preflight first." exit 1 fi - log_info "✓ Working tree clean" + (cd "$WORK_DIR" && "$@") +} - # Check gh auth +# Cleanup worktree +cmd_cleanup() { + if [[ -f "$WORKTREE_MARKER" ]]; then + local worktree_path + worktree_path=$(cat "$WORKTREE_MARKER") + if [[ -d "$worktree_path" ]]; then + log_info "Removing worktree at $worktree_path..." + git worktree remove "$worktree_path" --force 2>/dev/null || true + log_info "✓ Worktree removed" + fi + rm -f "$WORKTREE_MARKER" + else + log_info "No worktree to clean up" + fi +} + +cmd_preflight() { + log_info "Running pre-flight checks..." + + # Check gh auth first (doesn't require being in worktree) if ! gh auth status &>/dev/null; then log_error "GitHub CLI not authenticated. Run 'gh auth login' first." exit 1 fi log_info "✓ GitHub CLI authenticated" + # Setup worktree if not on dev + setup_worktree + + # Check clean working tree in the work directory + if [[ -n $(run_in_workdir git status --porcelain) ]]; then + log_error "Working tree is not clean. Commit or stash changes first." + exit 1 + fi + log_info "✓ Working tree clean" + # Fetch latest from origin log_info "Fetching latest from origin..." - git fetch origin + run_in_workdir git fetch origin # Back-merge: master -> staging (if needed) log_info "Checking if master needs to be merged into staging..." - MASTER_AHEAD=$(git rev-list --count origin/staging..origin/master) + MASTER_AHEAD=$(run_in_workdir git rev-list --count origin/staging..origin/master) if [[ "$MASTER_AHEAD" -gt 0 ]]; then log_info "Merging origin/master into staging ($MASTER_AHEAD commits)..." - git checkout staging - git merge origin/master -m "Merge master into staging" - git push origin staging - git checkout dev + run_in_workdir git checkout staging + run_in_workdir git merge origin/master -m "Merge master into staging" + run_in_workdir git push origin staging + run_in_workdir git checkout dev else log_info "✓ staging is up to date with master" fi # Back-merge: staging -> dev (if needed) log_info "Checking if staging needs to be merged into dev..." - STAGING_AHEAD=$(git rev-list --count origin/dev..origin/staging) + STAGING_AHEAD=$(run_in_workdir git rev-list --count origin/dev..origin/staging) if [[ "$STAGING_AHEAD" -gt 0 ]]; then log_info "Merging origin/staging into dev ($STAGING_AHEAD commits)..." - git merge origin/staging -m "Merge staging into dev" - git push origin dev + run_in_workdir git merge origin/staging -m "Merge staging into dev" + run_in_workdir git push origin dev else log_info "✓ dev is up to date with staging" fi # Pull latest dev log_info "Pulling latest origin/dev..." - git pull origin dev + run_in_workdir git pull origin dev log_info "✓ Pre-flight checks complete" + + # Output the work directory for subsequent commands + echo "WORK_DIR=$WORK_DIR" } cmd_version() { @@ -97,25 +171,31 @@ cmd_version() { exit 1 fi + # Ensure we're working in the right directory + setup_worktree + log_info "Bumping $VERSION_TYPE version..." - pnpm version "$VERSION_TYPE" + run_in_workdir pnpm version "$VERSION_TYPE" - NEW_VERSION=$(node -p "require('./package.json').version") + NEW_VERSION=$(run_in_workdir node -p "require('./package.json').version") log_info "New version: v$NEW_VERSION" log_info "Pushing to origin with tags..." - git push origin dev --follow-tags + run_in_workdir git push origin dev --follow-tags echo "$NEW_VERSION" } cmd_merge_staging() { + # Ensure we're working in the right directory + setup_worktree + log_info "Merging dev into staging..." - git checkout staging - git merge dev -m "Merge dev into staging for release" - git push origin staging - git checkout dev + run_in_workdir git checkout staging + run_in_workdir git merge dev -m "Merge dev into staging for release" + run_in_workdir git push origin staging + run_in_workdir git checkout dev log_info "✓ dev merged into staging" } @@ -219,6 +299,9 @@ case "${1:-}" in create-pr) cmd_create_pr "${2:-}" "${3:-}" ;; + cleanup) + cmd_cleanup + ;; *) echo "Usage: $0 [args]" echo "" @@ -229,6 +312,7 @@ case "${1:-}" in echo " fetch-draft Fetch draft release body (JSON)" echo " publish Publish release" echo " create-pr Create deploy PR" + echo " cleanup Remove worktree if created" exit 1 ;; esac From f9d66c13f5885af02781d1ce6fcb56c24f725ba9 Mon Sep 17 00:00:00 2001 From: Paul Wackerow <54227730+wackerow@users.noreply.github.com> Date: Wed, 7 Jan 2026 16:39:07 -0800 Subject: [PATCH 3/5] feat: add --dry-run flag --- .claude/commands/prepare-release.md | 19 ++-- src/scripts/prepare-release.sh | 158 ++++++++++++++++++++-------- 2 files changed, 125 insertions(+), 52 deletions(-) diff --git a/.claude/commands/prepare-release.md b/.claude/commands/prepare-release.md index ac5ee4a3298..b0496342e35 100644 --- a/.claude/commands/prepare-release.md +++ b/.claude/commands/prepare-release.md @@ -1,7 +1,7 @@ --- description: Prepare a release - version bump, branch sync, release notes cleanup, and deploy PR allowed-tools: Bash, Read, Write, AskUserQuestion -argument-hints: --major|--minor|--patch +argument-hints: --dry-run|--major|--minor|--patch --- # Prepare Release Command @@ -12,24 +12,29 @@ Automates the ethereum.org deployment workflow using `src/scripts/prepare-releas Details for $ARGUMENTS +- `--dry-run` - Show what would happen without making any changes to remote - `--major` - Major release (breaking/stack changes) - `--minor` - Minor release (new features, content, translations) - `--patch` - Patch release (bug fixes, typos, small updates) - _(no flag)_ - Analyze changes and suggest version type +**Dry-run mode**: If `--dry-run` is in `$ARGUMENTS`, pass it as the first argument to ALL script commands. This shows what would happen without pushing to remote or creating PRs. + ## Execution Flow ### Step 1: Pre-flight Checks +First, check if `--dry-run` is in `$ARGUMENTS`. If so, set `DRY_RUN_FLAG="--dry-run"`, otherwise set it to empty string. + Run the script to verify environment and sync branches: ```bash -./src/scripts/prepare-release.sh preflight +./src/scripts/prepare-release.sh $DRY_RUN_FLAG preflight ``` This handles: `gh` authenticated, create worktree if not on `dev`, clean working tree, back-merge `master` → `staging` → `dev`, pull latest. -**Note**: The script can run from any branch. If not on `dev`, it creates a worktree at `../worktrees/ethereum-org-dev` and performs all operations there. +**Note**: The script can run from any branch. If not on `dev`, it creates a worktree at `/tmp/claude/worktrees/ethereum-org-dev` and performs all operations there. If this fails, stop and report the error. @@ -51,13 +56,13 @@ Extract from `$ARGUMENTS` and proceed to Step 3. ### Step 3: Version Bump ```bash -VERSION=$(./src/scripts/prepare-release.sh version ) +VERSION=$(./src/scripts/prepare-release.sh $DRY_RUN_FLAG version ) ``` ### Step 4: Merge to Staging ```bash -./src/scripts/prepare-release.sh merge-staging +./src/scripts/prepare-release.sh $DRY_RUN_FLAG merge-staging ``` ### Step 5: Fetch Draft Release @@ -114,13 +119,13 @@ EOF ### Step 7: Publish Release ```bash -RELEASE_URL=$(./src/scripts/prepare-release.sh publish "$VERSION" "$DRAFT_TAG" /tmp/claude/release-notes.md) +RELEASE_URL=$(./src/scripts/prepare-release.sh $DRY_RUN_FLAG publish "$VERSION" "$DRAFT_TAG" /tmp/claude/release-notes.md) ``` ### Step 8: Create Deploy PR ```bash -PR_URL=$(./src/scripts/prepare-release.sh create-pr "$VERSION" /tmp/claude/release-notes.md) +PR_URL=$(./src/scripts/prepare-release.sh $DRY_RUN_FLAG create-pr "$VERSION" /tmp/claude/release-notes.md) ``` ### Step 9: Cleanup Worktree diff --git a/src/scripts/prepare-release.sh b/src/scripts/prepare-release.sh index 025f436fbfb..6a9b829adda 100755 --- a/src/scripts/prepare-release.sh +++ b/src/scripts/prepare-release.sh @@ -5,15 +5,19 @@ set -euo pipefail # Handles deterministic git/gh operations for ethereum.org releases # # Usage: -# ./src/scripts/prepare-release.sh preflight # Run pre-flight checks and back-merge sync -# ./src/scripts/prepare-release.sh version # Bump version (major|minor|patch) and push -# ./src/scripts/prepare-release.sh merge-staging # Merge dev into staging -# ./src/scripts/prepare-release.sh fetch-draft # Fetch draft release body -# ./src/scripts/prepare-release.sh publish # Publish release -# ./src/scripts/prepare-release.sh create-pr # Create deploy PR +# ./src/scripts/prepare-release.sh [--dry-run] preflight # Run pre-flight checks and back-merge sync +# ./src/scripts/prepare-release.sh [--dry-run] version # Bump version (major|minor|patch) and push +# ./src/scripts/prepare-release.sh [--dry-run] merge-staging # Merge dev into staging +# ./src/scripts/prepare-release.sh [--dry-run] fetch-draft # Fetch draft release body +# ./src/scripts/prepare-release.sh [--dry-run] publish # Publish release +# ./src/scripts/prepare-release.sh [--dry-run] create-pr # Create deploy PR # ./src/scripts/prepare-release.sh cleanup # Remove worktree if created +# +# Options: +# --dry-run Show what would be done without making any changes to remote REPO="ethereum/ethereum-org-website" +DRY_RUN=false # Worktree configuration REPO_ROOT=$(git rev-parse --show-toplevel) @@ -27,11 +31,35 @@ WORK_DIR="" RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' +BLUE='\033[0;34m' NC='\033[0m' # No Color log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } log_error() { echo -e "${RED}[ERROR]${NC} $1"; } +log_dry() { echo -e "${BLUE}[DRY-RUN]${NC} Would run: $1"; } + +# Run a command, or just log it if in dry-run mode +run_or_dry() { + if [[ "$DRY_RUN" == "true" ]]; then + log_dry "$*" + else + "$@" + fi +} + +# Run a command in workdir, or just log it if in dry-run mode +run_in_workdir_or_dry() { + if [[ -z "$WORK_DIR" ]]; then + log_error "WORK_DIR not set. Run preflight first." + exit 1 + fi + if [[ "$DRY_RUN" == "true" ]]; then + log_dry "(in $WORK_DIR) $*" + else + (cd "$WORK_DIR" && "$@") + fi +} # Setup worktree for dev branch if not already on dev setup_worktree() { @@ -129,10 +157,10 @@ cmd_preflight() { MASTER_AHEAD=$(run_in_workdir git rev-list --count origin/staging..origin/master) if [[ "$MASTER_AHEAD" -gt 0 ]]; then log_info "Merging origin/master into staging ($MASTER_AHEAD commits)..." - run_in_workdir git checkout staging - run_in_workdir git merge origin/master -m "Merge master into staging" - run_in_workdir git push origin staging - run_in_workdir git checkout dev + run_in_workdir_or_dry git checkout staging + run_in_workdir_or_dry git merge origin/master -m "Merge master into staging" + run_in_workdir_or_dry git push origin staging + run_in_workdir_or_dry git checkout dev else log_info "✓ staging is up to date with master" fi @@ -142,15 +170,17 @@ cmd_preflight() { STAGING_AHEAD=$(run_in_workdir git rev-list --count origin/dev..origin/staging) if [[ "$STAGING_AHEAD" -gt 0 ]]; then log_info "Merging origin/staging into dev ($STAGING_AHEAD commits)..." - run_in_workdir git merge origin/staging -m "Merge staging into dev" - run_in_workdir git push origin dev + run_in_workdir_or_dry git merge origin/staging -m "Merge staging into dev" + run_in_workdir_or_dry git push origin dev else log_info "✓ dev is up to date with staging" fi - # Pull latest dev - log_info "Pulling latest origin/dev..." - run_in_workdir git pull origin dev + # Pull latest dev (skip in dry-run since we didn't actually merge) + if [[ "$DRY_RUN" != "true" ]]; then + log_info "Pulling latest origin/dev..." + run_in_workdir git pull origin dev + fi log_info "✓ Pre-flight checks complete" @@ -174,14 +204,30 @@ cmd_version() { # Ensure we're working in the right directory setup_worktree - log_info "Bumping $VERSION_TYPE version..." - run_in_workdir pnpm version "$VERSION_TYPE" + if [[ "$DRY_RUN" == "true" ]]; then + # In dry-run, calculate what the new version would be without changing anything + CURRENT_VERSION=$(run_in_workdir node -p "require('./package.json').version") + log_info "Current version: v$CURRENT_VERSION" + log_dry "pnpm version $VERSION_TYPE" + # Calculate next version + IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION" + case "$VERSION_TYPE" in + major) NEW_VERSION="$((MAJOR + 1)).0.0" ;; + minor) NEW_VERSION="$MAJOR.$((MINOR + 1)).0" ;; + patch) NEW_VERSION="$MAJOR.$MINOR.$((PATCH + 1))" ;; + esac + log_info "Would bump to: v$NEW_VERSION" + log_dry "git push origin dev --follow-tags" + else + log_info "Bumping $VERSION_TYPE version..." + run_in_workdir pnpm version "$VERSION_TYPE" - NEW_VERSION=$(run_in_workdir node -p "require('./package.json').version") - log_info "New version: v$NEW_VERSION" + NEW_VERSION=$(run_in_workdir node -p "require('./package.json').version") + log_info "New version: v$NEW_VERSION" - log_info "Pushing to origin with tags..." - run_in_workdir git push origin dev --follow-tags + log_info "Pushing to origin with tags..." + run_in_workdir git push origin dev --follow-tags + fi echo "$NEW_VERSION" } @@ -192,10 +238,10 @@ cmd_merge_staging() { log_info "Merging dev into staging..." - run_in_workdir git checkout staging - run_in_workdir git merge dev -m "Merge dev into staging for release" - run_in_workdir git push origin staging - run_in_workdir git checkout dev + run_in_workdir_or_dry git checkout staging + run_in_workdir_or_dry git merge dev -m "Merge dev into staging for release" + run_in_workdir_or_dry git push origin staging + run_in_workdir_or_dry git checkout dev log_info "✓ dev merged into staging" } @@ -240,16 +286,22 @@ cmd_publish() { log_info "Publishing release v$VERSION..." - gh release edit "$DRAFT_TAG" \ - --repo "$REPO" \ - --tag "v$VERSION" \ - --title "v$VERSION" \ - --notes-file "$BODY_FILE" \ - --draft=false \ - --latest - - log_info "✓ Release v$VERSION published" - echo "https://github.com/$REPO/releases/tag/v$VERSION" + if [[ "$DRY_RUN" == "true" ]]; then + log_dry "gh release edit $DRAFT_TAG --repo $REPO --tag v$VERSION --title v$VERSION --notes-file $BODY_FILE --draft=false --latest" + log_info "✓ Would publish release v$VERSION" + echo "https://github.com/$REPO/releases/tag/v$VERSION" + else + gh release edit "$DRAFT_TAG" \ + --repo "$REPO" \ + --tag "v$VERSION" \ + --title "v$VERSION" \ + --notes-file "$BODY_FILE" \ + --draft=false \ + --latest + + log_info "✓ Release v$VERSION published" + echo "https://github.com/$REPO/releases/tag/v$VERSION" + fi } cmd_create_pr() { @@ -268,17 +320,30 @@ cmd_create_pr() { log_info "Creating deploy PR for v$VERSION..." - PR_URL=$(gh pr create \ - --repo "$REPO" \ - --base master \ - --head staging \ - --title "Deploy v$VERSION" \ - --body-file "$BODY_FILE") - - log_info "✓ Deploy PR created" - echo "$PR_URL" + if [[ "$DRY_RUN" == "true" ]]; then + log_dry "gh pr create --repo $REPO --base master --head staging --title \"Deploy v$VERSION\" --body-file $BODY_FILE" + log_info "✓ Would create deploy PR" + echo "https://github.com/$REPO/pull/XXXX (dry-run)" + else + PR_URL=$(gh pr create \ + --repo "$REPO" \ + --base master \ + --head staging \ + --title "Deploy v$VERSION" \ + --body-file "$BODY_FILE") + + log_info "✓ Deploy PR created" + echo "$PR_URL" + fi } +# Parse --dry-run flag +if [[ "${1:-}" == "--dry-run" ]]; then + DRY_RUN=true + log_warn "DRY-RUN MODE: No changes will be made to remote" + shift +fi + # Main command router case "${1:-}" in preflight) @@ -303,7 +368,10 @@ case "${1:-}" in cmd_cleanup ;; *) - echo "Usage: $0 [args]" + echo "Usage: $0 [--dry-run] [args]" + echo "" + echo "Options:" + echo " --dry-run Show what would be done without making changes" echo "" echo "Commands:" echo " preflight Run pre-flight checks and back-merge sync" From 1059c2a7182639b927ce2f74fdde34e9760150e9 Mon Sep 17 00:00:00 2001 From: Paul Wackerow <54227730+wackerow@users.noreply.github.com> Date: Wed, 7 Jan 2026 16:49:53 -0800 Subject: [PATCH 4/5] patch: node command syntax --- src/scripts/prepare-release.sh | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/scripts/prepare-release.sh b/src/scripts/prepare-release.sh index 6a9b829adda..55f75b17aef 100755 --- a/src/scripts/prepare-release.sh +++ b/src/scripts/prepare-release.sh @@ -250,7 +250,7 @@ cmd_fetch_draft() { log_info "Fetching draft release..." # Get all releases and find the draft one - DRAFT_RELEASE=$(gh release list --repo "$REPO" --json tagName,isDraft,body,name --limit 10 | \ + DRAFT_TAG=$(gh release list --repo "$REPO" --json tagName,isDraft --limit 10 | \ node -e " const data = JSON.parse(require('fs').readFileSync(0, 'utf8')); const draft = data.find(r => r.isDraft); @@ -258,15 +258,16 @@ cmd_fetch_draft() { console.error('No draft release found'); process.exit(1); } - console.log(JSON.stringify(draft)); + console.log(draft.tagName); ") - if [[ -z "$DRAFT_RELEASE" ]]; then + if [[ -z "$DRAFT_TAG" ]]; then log_error "No draft release found. Ensure Release Drafter workflow has run." exit 1 fi - echo "$DRAFT_RELEASE" + # Get the full release details including body and output as JSON + gh release view "$DRAFT_TAG" --repo "$REPO" --json tagName,body } cmd_publish() { From d3dfbfd68eb6c38fbfcc334bebd0d906908a717e Mon Sep 17 00:00:00 2001 From: Paul Wackerow <54227730+wackerow@users.noreply.github.com> Date: Wed, 7 Jan 2026 17:45:47 -0800 Subject: [PATCH 5/5] fix: improve prepare-release reliability - Use pushd/popd instead of subshell for run_in_workdir (fixes pnpm hanging in subshell with lifecycle scripts) - Auto-recover dirty worktree from interrupted runs - Add 'reset' command for manual recovery --- src/scripts/prepare-release.sh | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/scripts/prepare-release.sh b/src/scripts/prepare-release.sh index 55f75b17aef..14096d5089a 100755 --- a/src/scripts/prepare-release.sh +++ b/src/scripts/prepare-release.sh @@ -12,6 +12,7 @@ set -euo pipefail # ./src/scripts/prepare-release.sh [--dry-run] publish # Publish release # ./src/scripts/prepare-release.sh [--dry-run] create-pr # Create deploy PR # ./src/scripts/prepare-release.sh cleanup # Remove worktree if created +# ./src/scripts/prepare-release.sh reset # Reset worktree to clean state # # Options: # --dry-run Show what would be done without making any changes to remote @@ -100,16 +101,30 @@ setup_worktree() { mkdir -p /tmp/claude echo "$WORKTREE_DIR" > "$WORKTREE_MARKER" + # Check for and recover from interrupted runs (dirty worktree) + if [[ -n $(git -C "$WORK_DIR" status --porcelain) ]]; then + log_warn "Worktree has uncommitted changes from previous interrupted run" + log_warn "Resetting to clean state..." + git -C "$WORK_DIR" reset --hard HEAD + git -C "$WORK_DIR" checkout dev + log_info "✓ Worktree reset to clean state" + fi + log_info "✓ Worktree ready at $WORKTREE_DIR" } # Run a command in the work directory (worktree or repo root) +# Uses pushd/popd instead of subshell to avoid issues with npm lifecycle scripts run_in_workdir() { if [[ -z "$WORK_DIR" ]]; then log_error "WORK_DIR not set. Run preflight first." exit 1 fi - (cd "$WORK_DIR" && "$@") + pushd "$WORK_DIR" > /dev/null + "$@" + local exit_code=$? + popd > /dev/null + return $exit_code } # Cleanup worktree @@ -128,6 +143,16 @@ cmd_cleanup() { fi } +# Reset worktree to clean state (for recovery from interrupted runs) +cmd_reset() { + setup_worktree + log_info "Resetting worktree to clean state..." + git -C "$WORK_DIR" reset --hard HEAD + git -C "$WORK_DIR" checkout dev + git -C "$WORK_DIR" pull origin dev + log_info "✓ Worktree reset and updated" +} + cmd_preflight() { log_info "Running pre-flight checks..." @@ -368,6 +393,9 @@ case "${1:-}" in cleanup) cmd_cleanup ;; + reset) + cmd_reset + ;; *) echo "Usage: $0 [--dry-run] [args]" echo "" @@ -382,6 +410,7 @@ case "${1:-}" in echo " publish Publish release" echo " create-pr Create deploy PR" echo " cleanup Remove worktree if created" + echo " reset Reset worktree to clean state (recovery)" exit 1 ;; esac