diff --git a/scripts/RELEASE_RECOVERY.md b/scripts/RELEASE_RECOVERY.md new file mode 100644 index 000000000..23310e723 --- /dev/null +++ b/scripts/RELEASE_RECOVERY.md @@ -0,0 +1,235 @@ +# Freenet Release Recovery Guide + +When the release script fails mid-process, use this guide to complete the release manually. + +## Quick Reference + +```bash +# Check current state +gh pr view --json state,mergedAt +git tag -l "v*" | tail -5 +gh release list --limit 5 +cargo search freenet --limit 1 + +# Resume from where you left off +# See detailed steps below based on where the failure occurred +``` + +## Release Steps & Recovery + +### Step 1: PR Created but Not Merged + +**Symptoms:** +- PR exists but isn't merged +- CI may be failing + +**Recovery:** +1. Fix any CI failures +2. If conventional commits check failed: + ```bash + gh pr edit --title "chore: release X.Y.Z" + ``` +3. If other CI failures, fix code and push to release branch +4. Wait for PR to auto-merge or merge manually + +### Step 2: PR Merged but Tag Not Created + +**Symptoms:** +- PR is merged to main +- No git tag exists for version + +**Recovery:** +```bash +cd ~/code/freenet/freenet-core/main +git pull origin main +git tag v0.1.X +git push origin v0.1.X +``` + +### Step 3: Tag Created but GitHub Release Missing + +**Symptoms:** +- Git tag exists +- No GitHub release + +**Recovery:** +```bash +gh release create v0.1.X \ + --repo freenet/freenet-core \ + --title "v0.1.X" \ + --notes "$(gh api repos/freenet/freenet-core/releases/generate-notes \ + -f tag_name=v0.1.X -f target_commitish=main --jq .body)" +``` + +### Step 4: Release Created but Crates Not Published + +**Symptoms:** +- GitHub release exists +- Crates not on crates.io + +**Recovery:** +```bash +cd ~/code/freenet/freenet-core/main +git pull origin main + +# Publish freenet crate +cargo publish -p freenet + +# Publish fdev crate +cargo publish -p fdev + +# Verify +cargo search freenet --limit 1 +``` + +### Step 5: Crates Published but Local Not Deployed + +**Symptoms:** +- Everything published +- Local gateway not updated + +**Recovery:** +```bash +cd ~/code/freenet/freenet-core/main +cargo build --release --bin freenet + +# Deploy to gateway only +./scripts/deploy-local-gateway.sh + +# Deploy to all instances (gateway + 10 peers) +./scripts/deploy-local-gateway.sh --all-instances +``` + +### Step 6: Deployed but Matrix Not Announced + +**Symptoms:** +- Release complete +- No Matrix announcement + +**Recovery:** +```bash +matrix-commander -r '#freenet-locutus:matrix.org' -m "🎉 **Freenet v0.1.X Released!** + +đŸ“Ļ Published to crates.io: + â€ĸ freenet v0.1.X + â€ĸ fdev v0.Y.Z + +🔗 Release: https://github.com/freenet/freenet-core/releases/tag/v0.1.X + +[AI-assisted release announcement]" +``` + +## Common Issues + +### Issue: "Text file busy" during deployment + +**Cause:** Systemd services have `Restart=always` and keep respawning + +**Solution:** +```bash +# Stop all services and disable auto-restart +sudo systemctl stop freenet-gateway freenet-peer-{01..10} +sudo systemctl disable freenet-gateway freenet-peer-{01..10} + +# Wait for binary to be released +while sudo lsof /usr/local/bin/freenet; do sleep 1; done + +# Deploy new binary +sudo rm /usr/local/bin/freenet +sudo cp target/release/freenet /usr/local/bin/freenet + +# Re-enable and start +sudo systemctl enable freenet-gateway freenet-peer-{01..10} +sudo systemctl start freenet-gateway freenet-peer-{01..10} +``` + +### Issue: Conventional Commits CI failure + +**Cause:** PR title doesn't follow conventional commit format + +**Solution:** +```bash +gh pr edit --title "chore: release X.Y.Z" + +# Trigger CI rerun +git checkout release/vX.Y.Z +git commit --allow-empty -m "chore: trigger CI rerun" +git push origin release/vX.Y.Z +``` + +### Issue: Crates.io publishing fails + +**Cause:** Version already published, credentials issue, or dependency problems + +**Solution:** +```bash +# Check if already published +cargo search freenet --limit 1 + +# Verify credentials +cargo login + +# Check for dependency issues +cargo package --list -p freenet +cargo publish --dry-run -p freenet +``` + +## Full Manual Release Process + +If you need to do everything manually: + +```bash +# 1. Create PR and merge +cd ~/code/freenet/freenet-core/main +# Edit Cargo.toml versions manually +git checkout -b release/v0.1.X +git add -A +git commit -m "chore: release 0.1.X" +git push origin release/v0.1.X +gh pr create --title "chore: release 0.1.X" --body "Release v0.1.X" --base main + +# 2. Wait for CI and merge (or use gh pr merge --auto) + +# 3. Create tag +git checkout main +git pull +git tag v0.1.X +git push origin v0.1.X + +# 4. Create GitHub release +gh release create v0.1.X --repo freenet/freenet-core --generate-notes + +# 5. Publish crates +cargo publish -p freenet +cargo publish -p fdev + +# 6. Deploy locally +cargo build --release --bin freenet +./scripts/deploy-local-gateway.sh --all-instances + +# 7. Announce to Matrix +matrix-commander -r '#freenet-locutus:matrix.org' -m "..." +``` + +## Rollback + +If you need to rollback a release: + +```bash +./scripts/release-rollback.sh --version 0.1.X + +# To also yank from crates.io (cannot be undone!) +./scripts/release-rollback.sh --version 0.1.X --yank-crates +``` + +## Verification Checklist + +After recovery, verify: + +- [ ] PR merged: `gh pr view --json state` +- [ ] Tag exists: `git tag -l "v0.1.X"` +- [ ] GitHub release: `gh release view v0.1.X` +- [ ] Crates published: `cargo search freenet --limit 1` +- [ ] Local gateway updated: `/usr/local/bin/freenet --version` +- [ ] Services running: `systemctl status freenet-gateway freenet-peer-01` +- [ ] Matrix announced: Check #freenet-locutus channel diff --git a/scripts/deploy-local-gateway.sh b/scripts/deploy-local-gateway.sh index 19217215b..a731dcdd9 100755 --- a/scripts/deploy-local-gateway.sh +++ b/scripts/deploy-local-gateway.sh @@ -14,6 +14,8 @@ BINARY_PATH="" SERVICE_NAME="freenet-gateway" INSTALL_PATH="/usr/local/bin/freenet" DRY_RUN=false +ALL_INSTANCES=false +VERIFY_VERSION=true show_help() { echo "Deploy Freenet Gateway Locally" @@ -24,11 +26,14 @@ show_help() { echo " --binary PATH Path to freenet binary (default: auto-detect from cargo build)" echo " --service NAME Service name (default: freenet-gateway)" echo " --install-path PATH Installation path (default: /usr/local/bin/freenet)" + echo " --all-instances Deploy to gateway + all peer instances (peer-01 to peer-10)" + echo " --no-verify Skip version verification after deployment" echo " --dry-run Show what would be done without executing" echo " --help Show this help" echo echo "Examples:" - echo " $0 # Use default cargo release binary" + echo " $0 # Deploy to gateway only" + echo " $0 --all-instances # Deploy to gateway + all 10 peers" echo " $0 --binary ./target/release/freenet # Specify custom binary" echo " $0 --dry-run # Preview actions" echo @@ -53,6 +58,14 @@ while [[ $# -gt 0 ]]; do INSTALL_PATH="$2" shift 2 ;; + --all-instances) + ALL_INSTANCES=true + shift + ;; + --no-verify) + VERIFY_VERSION=false + shift + ;; --dry-run) DRY_RUN=true shift @@ -149,20 +162,61 @@ echo check_privileges +# Wait for binary to be released by all processes +wait_for_binary_release() { + local max_wait=30 + local waited=0 + + while sudo lsof "$INSTALL_PATH" &>/dev/null; do + if [[ $waited -ge $max_wait ]]; then + echo "âš ī¸ Timeout waiting for binary to be released" + echo " Processes still using $INSTALL_PATH:" + sudo lsof "$INSTALL_PATH" || true + return 1 + fi + + if [[ $waited -eq 0 ]]; then + echo -n " Waiting for binary to be released" + fi + echo -n "." + sleep 1 + ((waited++)) + done + + if [[ $waited -gt 0 ]]; then + echo " ✓" + fi + return 0 +} + # Service management functions stop_service() { + local service_arg="$1" + case "$SERVICE_MANAGER" in systemd) - if systemctl is-active --quiet "$SERVICE_NAME.service" 2>/dev/null; then - echo -n " Stopping systemd service... " + if systemctl is-active --quiet "$service_arg.service" 2>/dev/null; then + echo -n " Stopping systemd service ($service_arg)... " if [[ "$DRY_RUN" == "true" ]]; then echo "[DRY RUN]" else - sudo systemctl stop "$SERVICE_NAME.service" + # Temporarily disable to prevent auto-restart + local was_enabled=false + if systemctl is-enabled --quiet "$service_arg.service" 2>/dev/null; then + was_enabled=true + sudo systemctl disable "$service_arg.service" --quiet + fi + + sudo systemctl stop "$service_arg.service" echo "✓" + + # Store enabled state for later restoration + if [[ "$was_enabled" == "true" ]]; then + echo "$service_arg" >> /tmp/freenet-deploy-reenable.list + fi fi else - echo " â„šī¸ Service not running, skipping stop" + echo " â„šī¸ Service $service_arg not running, skipping stop" fi ;; launchd) @@ -191,18 +245,25 @@ stop_service() { } start_service() { + local service_arg="$1" + case "$SERVICE_MANAGER" in systemd) - if systemctl list-unit-files | grep -q "^$SERVICE_NAME.service" 2>/dev/null; then - echo -n " Starting systemd service... " + if systemctl list-unit-files | grep -q "^$service_arg.service" 2>/dev/null; then + echo -n " Starting systemd service ($service_arg)... " if [[ "$DRY_RUN" == "true" ]]; then echo "[DRY RUN]" else - sudo systemctl start "$SERVICE_NAME.service" + # Re-enable if it was enabled before + if [[ -f /tmp/freenet-deploy-reenable.list ]] && grep -q "^$service_arg$" /tmp/freenet-deploy-reenable.list; then + sudo systemctl enable "$service_arg.service" --quiet + fi + + sudo systemctl start "$service_arg.service" echo "✓" fi else - echo " âš ī¸ Service unit file not found, cannot start automatically" + echo " âš ī¸ Service unit file not found for $service_arg, cannot start automatically" echo " Start manually with: $INSTALL_PATH [args]" fi ;; @@ -228,25 +289,33 @@ start_service() { } verify_service() { + local service_arg="$1" + local expected_version="$2" + case "$SERVICE_MANAGER" in systemd) - if systemctl list-unit-files | grep -q "^$SERVICE_NAME.service" 2>/dev/null; then - echo -n " Verifying service status... " + if systemctl list-unit-files | grep -q "^$service_arg.service" 2>/dev/null; then + echo -n " Verifying service status ($service_arg)... " sleep 2 # Give service time to start - if systemctl is-active --quiet "$SERVICE_NAME.service"; then + if systemctl is-active --quiet "$service_arg.service"; then local running_version=$("$INSTALL_PATH" --version 2>&1 | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+' | head -1 || echo "unknown") + + # Verify version matches + if [[ "$VERIFY_VERSION" == "true" ]] && [[ "$expected_version" != "unknown" ]] && [[ "$running_version" != "$expected_version" ]]; then + echo "✗" + echo " âš ī¸ Version mismatch!" + echo " Expected: $expected_version" + echo " Got: $running_version" + return 1 + fi + echo "✓" echo " ✓ Version: $running_version" echo " ✓ Service: Running" - - # Show recent logs - echo - echo "Recent logs:" - sudo journalctl -u "$SERVICE_NAME.service" -n 10 --no-pager else echo "✗" echo " âš ī¸ Service failed to start" - echo " Check logs with: sudo journalctl -u $SERVICE_NAME.service -n 50" + echo " Check logs with: sudo journalctl -u $service_arg.service -n 50" return 1 fi fi @@ -261,11 +330,30 @@ verify_service() { esac } +# Determine which services to deploy +SERVICES_TO_DEPLOY=("$SERVICE_NAME") +if [[ "$ALL_INSTANCES" == "true" ]]; then + SERVICES_TO_DEPLOY=(freenet-gateway freenet-peer-{01..10}) +fi + +# Clear reenable list from previous runs +rm -f /tmp/freenet-deploy-reenable.list + # Main deployment steps echo "Deployment Steps:" echo -stop_service +# Stop all services +for service in "${SERVICES_TO_DEPLOY[@]}"; do + stop_service "$service" +done + +# Wait for binary to be released +if [[ "$DRY_RUN" == "false" ]]; then + wait_for_binary_release || { + echo "âš ī¸ Failed to wait for binary release. Proceeding anyway..." + } +fi echo -n " Installing binary to $INSTALL_PATH... " if [[ "$DRY_RUN" == "true" ]]; then @@ -276,6 +364,9 @@ else sudo cp "$INSTALL_PATH" "$INSTALL_PATH.backup" fi + # Remove old binary first + sudo rm -f "$INSTALL_PATH" + sudo cp "$BINARY_PATH" "$INSTALL_PATH" sudo chmod 755 "$INSTALL_PATH" @@ -289,12 +380,22 @@ else echo "✓" fi -start_service +# Start all services +for service in "${SERVICES_TO_DEPLOY[@]}"; do + start_service "$service" +done +# Verify services if [[ "$DRY_RUN" == "false" ]] && [[ "$SERVICE_MANAGER" == "systemd" ]]; then - verify_service + echo + for service in "${SERVICES_TO_DEPLOY[@]}"; do + verify_service "$service" "$BINARY_VERSION" || true + done fi +# Clean up reenable list +rm -f /tmp/freenet-deploy-reenable.list + echo echo "✅ Deployment complete!" echo diff --git a/scripts/release-rollback.sh b/scripts/release-rollback.sh new file mode 100755 index 000000000..e0a395397 --- /dev/null +++ b/scripts/release-rollback.sh @@ -0,0 +1,189 @@ +#!/bin/bash +# Freenet Release Rollback Script +# Rolls back a failed or problematic release + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Find the git repository root +if ! PROJECT_ROOT="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null)"; then + echo "Error: Not in a git repository" + exit 1 +fi + +VERSION="" +DRY_RUN=false +YANK_CRATES=false + +show_help() { + echo "Freenet Release Rollback Script" + echo + echo "Usage: $0 --version X.Y.Z [options]" + echo + echo "Rollback actions:" + echo " â€ĸ Delete git tag (local and remote)" + echo " â€ĸ Delete GitHub release" + echo " â€ĸ Optionally yank crates from crates.io (--yank-crates)" + echo + echo "Options:" + echo " --version X.Y.Z Version to rollback (required)" + echo " --yank-crates Yank crates from crates.io (optional, use with caution)" + echo " --dry-run Show what would be done without executing" + echo " --help Show this help" + echo + echo "Example: $0 --version 0.1.32" + echo + echo "âš ī¸ WARNING: This is a destructive operation!" + echo " Use with caution. Yanking from crates.io cannot be undone." +} + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + --version) + VERSION="$2" + shift 2 + ;; + --yank-crates) + YANK_CRATES=true + shift + ;; + --dry-run) + DRY_RUN=true + shift + ;; + --help|-h) + show_help + exit 0 + ;; + *) + echo "Unknown option: $1" + show_help + exit 1 + ;; + esac +done + +if [[ -z "$VERSION" ]]; then + echo "Error: --version is required" + show_help + exit 1 +fi + +# Validate version format +if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Error: Version must be in format X.Y.Z (e.g., 0.1.32)" + exit 1 +fi + +TAG="v$VERSION" + +echo "Freenet Release Rollback" +echo "========================" +echo "Version: $VERSION" +echo "Tag: $TAG" +if [[ "$DRY_RUN" == "true" ]]; then + echo "Mode: DRY RUN" +fi +if [[ "$YANK_CRATES" == "true" ]]; then + echo "Yank crates: YES" +fi +echo + +# Confirmation prompt +if [[ "$DRY_RUN" == "false" ]]; then + echo "âš ī¸ WARNING: This will rollback release $VERSION" + if [[ "$YANK_CRATES" == "true" ]]; then + echo "âš ī¸ This includes YANKING crates from crates.io (cannot be undone)" + fi + echo + read -p "Are you sure you want to continue? (yes/no): " -r + if [[ ! $REPLY =~ ^yes$ ]]; then + echo "Aborted." + exit 1 + fi +fi + +# Delete local git tag +echo -n "[1/4] Deleting local git tag... " +if git rev-parse "$TAG" >/dev/null 2>&1; then + if [[ "$DRY_RUN" == "true" ]]; then + echo "[DRY RUN]" + else + git tag -d "$TAG" + echo "✓" + fi +else + echo "not found, skipping" +fi + +# Delete remote git tag +echo -n "[2/4] Deleting remote git tag... " +if git ls-remote --tags origin | grep -q "refs/tags/$TAG"; then + if [[ "$DRY_RUN" == "true" ]]; then + echo "[DRY RUN]" + else + git push origin --delete "$TAG" 2>&1 | grep -v "^remote:" || true + echo "✓" + fi +else + echo "not found, skipping" +fi + +# Delete GitHub release +echo -n "[3/4] Deleting GitHub release... " +if gh release view "$TAG" --repo freenet/freenet-core >/dev/null 2>&1; then + if [[ "$DRY_RUN" == "true" ]]; then + echo "[DRY RUN]" + else + gh release delete "$TAG" --repo freenet/freenet-core --yes + echo "✓" + fi +else + echo "not found, skipping" +fi + +# Yank crates from crates.io +if [[ "$YANK_CRATES" == "true" ]]; then + echo "[4/4] Yanking crates from crates.io..." + + # Calculate fdev version + IFS='.' read -r major minor patch <<< "$VERSION" + minor_plus_2=$((minor + 2)) + FDEV_VERSION="0.${minor_plus_2}.${patch}" + + echo -n " Yanking freenet v$VERSION... " + if [[ "$DRY_RUN" == "true" ]]; then + echo "[DRY RUN]" + else + if cargo yank --vers "$VERSION" freenet 2>&1 | grep -q "successfully yanked\|already yanked"; then + echo "✓" + else + echo "✗ (failed or not published)" + fi + fi + + echo -n " Yanking fdev v$FDEV_VERSION... " + if [[ "$DRY_RUN" == "true" ]]; then + echo "[DRY RUN]" + else + if cargo yank --vers "$FDEV_VERSION" fdev 2>&1 | grep -q "successfully yanked\|already yanked"; then + echo "✓" + else + echo "✗ (failed or not published)" + fi + fi +else + echo "[4/4] Skipping crate yanking (use --yank-crates to enable)" +fi + +echo +echo "✅ Rollback complete!" +echo +echo "Next steps:" +echo " â€ĸ Verify the tag and release are gone: gh release list --repo freenet/freenet-core" +echo " â€ĸ Check crates.io: https://crates.io/crates/freenet" +if [[ "$YANK_CRATES" == "false" ]]; then + echo " â€ĸ To yank crates, run: $0 --version $VERSION --yank-crates" +fi diff --git a/scripts/release.sh b/scripts/release.sh index 335a6f079..f85703138 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -18,6 +18,21 @@ DRY_RUN=false SKIP_TESTS=false DEPLOY_LOCAL=false DEPLOY_REMOTE=false +RESUME=false +STATE_FILE="" + +# Release steps for state tracking +declare -A RELEASE_STEPS=( + [1]="PR_CREATED" + [2]="PR_MERGED" + [3]="TAG_CREATED" + [4]="RELEASE_CREATED" + [5]="CRATES_PUBLISHED" + [6]="LOCAL_DEPLOYED" + [7]="MATRIX_ANNOUNCED" +) + +CURRENT_STEP=0 show_help() { echo "Freenet Release Script" @@ -33,6 +48,7 @@ show_help() { echo " --deploy-local Deploy to local gateway after release (optional)" echo " --deploy-remote Deploy to remote gateways after release (optional)" echo " --skip-tests Skip pre-release tests" + echo " --resume STATE_FILE Resume failed release from state file" echo " --dry-run Show what would be done without executing" echo " --help Show this help" echo @@ -64,6 +80,11 @@ while [[ $# -gt 0 ]]; do SKIP_TESTS=true shift ;; + --resume) + RESUME=true + STATE_FILE="$2" + shift 2 + ;; --dry-run) DRY_RUN=true shift