up 0.18.1

up is a tool to help you keep your machine up to date.
Documentation
#!/usr/bin/env zsh

# Publish a new release.

set -euo pipefail

# For docs see docs/RELEASE.md

autoload -U colors && colors

if [[ -n ${DEBUG:-} ]]; then
  set -x
fi

project_dir=${0:a:h:h}
cd $project_dir

# Deny all warnings in CI.
export RUSTFLAGS="-D warnings"

# Set trace logs for tests using test_log::test
export RUST_LOG=up=trace

# Don't send SIGTTOU to background jobs that write to the tty. Theoretically fixes this in busy commands:
# [1]  + 82656 suspended (tty output)  cargo run
# Note that this doesn't fix tty input issues, for those you need to make things not read from the
# tty.
stty -tostop

# Don't go to sleep while we're running the release script.
caffeinate -ds -w $$ &

binary_name=up
macos_binary=build/$binary_name-Darwin
linux_amd64_binary=build/$binary_name-Linux
linux_arm64_binary=build/$binary_name-Linux_arm64
task_schema_json=build/$binary_name-task-schema.json

main() {
  echo -e "${fg[magenta]}Publishing new version of the ${binary_name} CLI...${reset_color}"

  # Install nextest runner if missing: https://nexte.st
  (( $+commands[cargo-nextest] )) ||  brew install cargo-nextest
  (( $+commands[git-cliff] )) || brew install git-cliff
  (( $+commands[gh] )) || brew install gh

  # Delete stale files.
  log_section "Cleaning up files from last release..."
  rm -rfv build/
  rm -fv target/universal-apple-darwin/release/$binary_name
  mkdir -p build/

  if [[ $(git branch --show-current) != main ]]; then
    error "Not currently on the main branch."
  fi

  if [[ -n ${SKIP_GIT_DIFF_CHECK:-} ]]; then
    log_section "Skipping it changed file check as SKIP_GIT_DIFF_CHECK env var was set."
  else
    # Bail on uncommitted diffs.
    diff=$(git diff --color=always)
    if [[ -n $diff ]]; then
      error "${fg[cyan]}-> Repo has uncommitted diffs:${reset_color}
      $diff"
    fi
  fi

  if [[ -n ${SKIP_GIT_UNTRACKED_CHECK:-} ]]; then
    log_section "Skipping git untracked file check as SKIP_GIT_UNTRACKED_CHECK env var was set."
  else
    # Bail on untracked files.
    untracked=$(git ls-files . --exclude-standard --others | head)
    if [[ -n $untracked ]]; then
      error "Repo has untracked files:\n$untracked"
    fi
  fi

  # Takes a line like:
  # `## [0.13.4](https://github.com/gibfahn/up/releases/tag/0.13.4) (2023-02-07)`
  # and extracts the `0.13.4`.
  changelog_version=$(awk <CHANGELOG.md '{ if ($1 == "##") { print $2; exit; }}' | sed -E 's/\[([^]]+)].*/\1/')
  last_release=$(gh release list -L 1 | awk '{print $1}')
  cargo_toml_version=$(awk -F\" '/^version = /{print $2; exit}' Cargo.toml)

  # Bump version in Cargo.toml and changelog:

  if [[ $changelog_version != $last_release && $last_release != $cargo_toml_version ]]; then
    new_version=$changelog_version
    if [[ $(git log -1 --pretty=%s) == "chore: bump version to ${new_version?}" ]]; then
      log_section "Last commit bumped us to ${new_version?}, skipping changelog update..."
    else
      error "If you generated a bump version commit and then pushed another change to the branch,
      delete and recreate the bump version commit."
    fi
  else
    log_section "Updating changelog..."

    # First argument is major,minor, or patch, else we prompt for it.
    case ${1:-} in
      major | minor | patch) new_version=$(bump_version $1 $last_release) ;;
      "")
        default_new_release=$(bump_version patch $last_release)
        read "new_version?New version (current version: ${last_release?}, default new version: ${default_new_release}): "
        [[ -z $new_version ]] && new_version=$default_new_release
        ;;
      *) error "Unrecognized input ${1}" ;;
    esac

    git cliff --tag="${new_version?}" --prepend CHANGELOG.md "${last_release}"..

    log_section "Updating Cargo.toml..."
    gsed -i -E "0,/^version = \"${last_release?}\"\$/s//version = \"${new_version?}\"/" Cargo.toml
    log_and_run cargo check --release # Bumps version in lockfile too.

    # Update the command-line help text.
    cargo run -- doc markdown >docs/CommandLineHelp-macOS.md
    meta/cargo-docker --pull -- cargo run --color=always -- doc markdown >docs/CommandLineHelp-Linux.md

    log_section "Committing version updates..."
    git add Cargo.toml Cargo.lock CHANGELOG.md docs/CommandLineHelp-macOS.md docs/CommandLineHelp-Linux.md
    git commit -m "chore: bump version to ${new_version?}"
    git show --stat | cat # Check version is correct.
    echo >&2 "Does this look correct?"
  fi

  # Run unit and integration tests for macOS.
  if [[ -n ${SKIP_CARGO_TEST_CHECK:-} ]]; then
    log_section "Skipping tests as SKIP_CARGO_TEST_CHECK env var was set."
  else
    log_and_run cargo nextest run --release --run-ignored=all --no-fail-fast
  fi

  # Generate the JSON schema for a task.
  cargo run -- doc schema $task_schema_json

  log_section "Running end-to-end tests..."
  # Run End-to-End tests and build Linux binaries:
  log_and_run meta/bootstrap-test

  # Build Darwin release binaries (without the CI feature).
  log_and_run meta/build_macos
  # This was built as part of running e2e tests.
  cp target/universal-apple-darwin/release/up $macos_binary
  # This allows them to be downloaded as `up-$(uname)`.
  cp target/x86_64-unknown-linux-musl/release/up $linux_amd64_binary
  cp target/aarch64-unknown-linux-musl/release/up $linux_arm64_binary


  latest_crate_version=$(curl https://crates.io/api/v1/crates/up | jq -r .crate.newest_version)
  if [[ $latest_crate_version == $new_version ]]; then
    prompt_to_skip "Skipping cargo publish as latest release is already $latest_crate_version."
  else
    # Publish to crates.io:
    log_and_run cargo publish
  fi

  log_and_run git push up main

  log_and_run gh release create "${new_version?}" --target=main \
    --notes="$(git cliff --tag="${new_version?}" --strip=all "${last_release}"..)" \
    $macos_binary \
    $linux_amd64_binary \
    $linux_arm64_binary \
    $task_schema_json

  new_release=$(gh release list -L 1 | awk '{print $1}')
  gh release view $new_release
  if [[ $new_release != $new_version ]]; then
    error "Something went wrong, latest GitHub version is not what the script just released."
  fi

  # Pull in the tag we just created remotely.
  log_section "Fetching just-created tag..."
  git fetch --all
}

# Bump the major, minor, or patch (set by $1) version of the given version.
# $1: major|minor|patch , which number to bump.
# $2: the input version string, e.g. `1.2.3`.
bump_version() {
  case $1 in
    major) awk -F '.' '{print $1+1 ".0.0"}' <<<$2 ;;
    minor) awk -F '.' '{print $1 "." $2+1 ".0"}' <<<$2 ;;
    patch) awk -F '.' '{print $1 "." $2 "." $3+1}' <<<$2 ;;
    *) error "bump_version: unknown input ${2}" ;;
  esac
}

log_section() {
  echo "
${fg[cyan]}=> $*${reset_color}" >&2
}

log_and_run() {
  log_section "Running $*"
  time "$@"
}

# $1: Error message
# $2: Error code (default: 1).
error() {
  echo -e "${fg[red]}ERROR${reset_color}: $1" >&2
  exit "${2:-1}"
}

prompt_to_skip() {
  read "user_input?$1
  Press Enter to continue, type anything or press Ctrl-C to cancel: "
  if [[ -n ${user_input:-} ]]; then
    error "User entered text."
  fi
}

main "$@"