#!/bin/sh

# This script automates release creation:
# 1. Create release branch from target commit.
# 1a. Validate contents of target commit (just upgradeable_from currently).
# 2. Set toolkit version on branch.
# 3. Run tests.
# 4. Push (if -push) the branch so release-build-scripts repository [1] can see the commit from #2.
# 5. Trigger (if -push) toolkit packaging actions in release-build-scripts repository.
# 6. Tag the release (and push, if -push). [2]
# 7. Prepare the main branch for the next release cycle.
# 7a. Update upgradeable_form in control file.
# 7b. Set toolkit version to released version with '-dev' appended.
# 7c. Update Changelog.md .
# 7d. Push to and create pull request for post-$VERSION branch (if -push).
# 8. File issue for release tasks that are not yet automated (if -push).

# [1] We need a self-hosted runner for arm64 build, which we can only get with
#     a private repository, so we must delegate packaging to that.

# [2] This means we publish a tag before testing binaries.  We'd rather test first.
#     TODO How?
#     - Can we have release-build-scripts gh back to an action over here?
#     - Can we have a trigger that watches for release-build-scripts action to finish?

# Sample run:
# tools/release -n -push -version 1.11.0 9c2b04d

# git commit records these on commits (yes, all three).
# TODO What should we use?  I pulled this from the deb package metadata
EMAIL=hello@tigerdata.com
GIT_AUTHOR_NAME=tools/release
GIT_COMMITTER_NAME=$GIT_AUTHOR_NAME
export EMAIL GIT_AUTHOR_NAME GIT_COMMITTER_NAME

MAIN_BRANCH=main
BRANCH_BASENAME=forge-stable-
CONTROL=extension/timescaledb_toolkit.control
TOML=extension/Cargo.toml
UPGRADEABLE_FROM_RE="^# upgradeable_from = '[^']*'\$"
NEXT_RELEASE_RE='^## Next Release (Date TBD)'

. tools/dependencies.sh

set -ex

# TODO Install these into timescaledev/toolkit-builder image and delete this block.
if [ "$1" = setup ]; then
    # Install cargo set-version (and cargo install is not idempotent).
    if ! cargo help set-version > /dev/null; then
        cargo install --version =$CARGO_EDIT cargo-edit
    fi
    # Install gh
    gh=`basename $GH_DEB_URL`
    curl --fail -LO $GH_DEB_URL
    sha256sum -c - <<EOF
$GH_DEB_SHA256  $gh
EOF
    sudo dpkg -i $gh
    exit
fi

print() {
    printf '%s\n' "$*"
}

die() {
    st=${?:-0}
    if [ $st -eq 0 ]; then
        st=2
    fi
    print "$*" >&2
    exit $st
}

usage() {
    die 'release [-n] [-push] -version VERSION COMMIT'
}

# Return 0 iff working directory is clean.
# Also prints any diff.
assert_clean() {
    $nop git diff --exit-code
}

# Return 0 iff working directory is dirty.
# Also prints any diff.
assert_dirty() {
    [ -n "$nop" ] && return
    ! assert_clean
}

# Use start_commit, commit, and finish_commit to safely build a commit from
# multiple automated edits.
# - start_commit [file names]
#   Start a commit with the named changed files.
#   Any other edited file (dirty directory after commit) is an error.
# - commit [file names]
#   Amend the commit after each automated edit.
#   Any other edited file (dirty directory after commit) is an error.
# - finish_commit MESSAGE
#   Finalize the commit with the commit message MESSAGE.
#   Any edited files is an error.
start_commit() {
    [ -z "$_PENDING_COMMIT" ] || die 'BUG: start_commit called twice'
    _PENDING_COMMIT=1
    $nop git add "$@"
    $nop git commit -m pending
    assert_clean || die "working directory should be clean after commit $@"
}

commit() {
    [ -n "$_PENDING_COMMIT" ] || die 'BUG: commit called without start_commit'
    $nop git add "$@"
    $nop git commit --no-edit --amend
    assert_clean || die "working directory should be clean after commit $@"
}

finish_commit() {
    [ -n "$_PENDING_COMMIT" ] || die 'BUG: finish_commit called without start_commit'
    assert_clean || die "working directory should be clean to finish commit '$1'"
    _PENDING_COMMIT=
    (export GIT_COMMITTER_DATE="`date`" && $nop git commit --no-edit --amend "--date=$GIT_COMMITTER_DATE" -m "$1")
}

# Return 0 if this is a minor release (i.e. $PATCH is greater than zero).
release_is_minor() {
    [ $PATCH -eq 0 ]
}

# Super simple option processing.
push=false
while [ $# -gt 0 ]; do
    arg=$1
    shift
    case "$arg" in
        -n)
            dry_run_flag=--dry-run
            nop=:
            ;;

        # TODO Remove -y alias for -push .
        -push | -y)
            push=true
            ;;

        -version)
            VERSION=$1
            shift
            COMMIT=$1
            shift
            ;;

        *)
            usage
            ;;
    esac
done

[ -n "$VERSION" ] && [ -n "$COMMIT" ] || usage

# And away we go!

MAJOR=${VERSION%%.*}
minpat=${VERSION#*.}
MINOR=${minpat%.*}
PATCH=${minpat#*.}

POST_REL_BRANCH=post-$VERSION

# 0. Sanity-check the surroundings.
# working directory clean?
assert_clean || die 'cowardly refusing to operate on dirty working directory'

# 1. Create release branch from target commit.
branch="$BRANCH_BASENAME"$VERSION
$nop git checkout -b $branch $COMMIT

# Sanity-check the branch contents.
# control file matches expectations?
count=`grep -c "$UPGRADEABLE_FROM_RE" $CONTROL` || die "upgradeable_from line malformed"
if [ "$count" -ne 1 ]; then
    print >&2 "too many upgradeable_from lines matched:"
    grep >&2 "$UPGRADEABLE_FROM_RE" $CONTROL
    die
fi
# If we forget to update the Changelog (or forget to cherry-pick Changelog
# updates), show a clear error message rather than letting the ed script fail
# mysteriously.
grep -qs "$NEXT_RELEASE_RE" Changelog.md || die 'Changelod.md lacks "Next Release" section'

# 1a. Validate contents of target commit (just upgradeable_from currently).
if ! release_is_minor; then
    # Releasing e.g. 1.13.2 - this one might be a cherry-pick, so we need to ensure upgradeable from 1.13.1 .
    # It is conceivable that we could intend to release 1.17.1 without
    # allowing upgrade from 1.17.0, but we can cross that bridge if we come
    # to it.
    prev=$MAJOR.$MINOR.$(( PATCH - 1 ))
    # The set of lines matching this pattern is a subset of the set required in preflight above.
    grep -Eqs "^# upgradeable_from = '[^']*,?$prev[,']" $CONTROL || die "$prev missing from upgradeable_from "
fi
# Else releasing e.g. 1.13.0 - these are never cherrypicks and we automatically set upgradeable_from on main.

# 2. Set toolkit version.
cargo set-version $dry_run_flag -p timescaledb_toolkit $VERSION
assert_dirty || die "failed to set toolkit version to $VERSION in $TOML"
start_commit $TOML
# Update cargo.lock - this form of cargo update doesn't update dependency versions.
$nop cargo update -p timescaledb_toolkit
assert_dirty || die "failed to set toolkit version to $VERSION in Cargo.lock"
commit Cargo.lock
# Update Changelog.md .
branch_commit_date=`git log -1 --pretty=format:%as $branch_commit`
$nop ed Changelog.md <<EOF
/$NEXT_RELEASE_RE/
d
i
## [$VERSION](https://github.com/timescale/timescaledb-toolkit/releases/tag/$VERSION) ($branch_commit_date)
.
wq
EOF
assert_dirty || die 'failed to update Changelog.md for next release'
commit Changelog.md
finish_commit "release $VERSION"
$nop git show

# 3. Run tests.
for pg in $PG_VERSIONS; do
    $nop tools/build -pg$pg test-extension
    $nop rm -rf target # we only have limited disk space on github's runner
done
assert_clean || die 'tools/build should not dirty the working directory'

# 4. Push the branch
if $push; then
    $nop git push origin $branch
fi

# 5. Trigger toolkit packaging actions in release-build-scripts repository.
branch_commit=`git log -1 --pretty=format:%h`
if $push; then
    $nop gh workflow run toolkit-package.yml \
        -R timescale/release-build-scripts \
        -r $MAIN_BRANCH \
        -f version=$VERSION \
        -f commit-id=$branch_commit \
        -f upload-artifacts=true
fi

# 6. Tag the release.
$nop git tag $VERSION
if $push; then
    $nop git push origin $VERSION
    # TODO gh release
#     ed -s > release-notes Changelog.md <<EOF
# /^## \[[^]]*](https:..github.com.timescale.timescaledb-toolkit.releases.tag/
# +,/^## \[[^]]*](https:..github.com.timescale.timescaledb-toolkit.releases.tag/p
# EOF
#     gh release create -dF release-notes --target $VERSION $VERSION
fi

# 7. Prepare the main branch for the next release cycle.
# Github action gives us a shallow checkout which we must deepen before we can push changes.
$nop git fetch --deepen=2147483647 origin $MAIN_BRANCH
$nop git checkout -b $POST_REL_BRANCH $MAIN_BRANCH

# 7a. Update upgradeable_form in control file.
$nop sed --in-place "/$UPGRADEABLE_FROM_RE/ { s/'\$/, $VERSION'/ }" $CONTROL
assert_dirty || die "failed to update $CONTROL for next release"
start_commit $CONTROL

if release_is_minor; then
    # 7b. Set toolkit version to released version with '-dev' appended.
    # Skip for patch releases:  we've already started the next minor version in that case.
    DEV_VERSION=$MAJOR.$(( MINOR + 1 )).0-dev
    cargo set-version $dry_run_flag -p timescaledb_toolkit $DEV_VERSION
    assert_dirty || die "failed to set toolkit version to $DEV_VERSION in $TOML"
    commit $TOML
    # Update cargo.lock - this form of cargo update doesn't update dependency versions.
    $nop cargo update -p timescaledb_toolkit
    assert_dirty || die "failed to set toolkit version to $DEV_VERSION in Cargo.lock"
    commit Cargo.lock

    # 7c. Update Changelog.md .
    # Skip for patch releases as it's not clear how to automate the cherry-pick case.
    # For now, we just have to add patch releases to the main Changelog manually.
    # The edit we apply here for minor releases would be wrong in the
    # cherry-pick case, as it would erroneously list the skipped changes on
    # main as part of the patch release.  This script has no way to
    # distinguish blocks of text belonging to one release from another, so
    # automating that case is probably not feasible.
    # TODO Or is it?
    branch_commit_date=`git log -1 --pretty=format:%as $branch_commit`
    $nop ed Changelog.md <<EOF
/$NEXT_RELEASE_RE/
a

#### New experimental features

#### Bug fixes

#### Other notable changes

#### Shout-outs

**Full Changelog**: [TODO]

## [$VERSION](https://github.com/timescale/timescaledb-toolkit/releases/tag/$VERSION) ($branch_commit_date)
.
wq
EOF
    assert_dirty || die 'failed to update Changelog.md for next release'
    commit Changelog.md

    finish_commit "start $DEV_VERSION"
else
    finish_commit "add $VERSION to upgradeable_from"
fi
$nop git show

# We've had a lot of trouble with the rest of these, so let's continue on
# error, to see what works and what doesn't.
set +e
# TODO Carefully attempt to report errors but keep going on all steps after we push the tag in step 6.

if $push; then
    # 7d. Push to $POST_REL_BRANCH branch.
    $nop git push origin HEAD:$POST_REL_BRANCH

    # Run these next steps as github-actions[bot]
    GITHUB_TOKEN="$ACTIONS_GITHUB_TOKEN"

    $nop gh pr create -R timescale/timescaledb-toolkit -B $MAIN_BRANCH --fill -H $POST_REL_BRANCH

    # 8. File issue for release tasks that are not yet automated.
    $nop gh issue create -R timescale/timescaledb-toolkit -F- -t "Release $VERSION" <<EOF
- [ ] Docker HA image

[Sample pull request](https://github.com/timescale/timescaledb-docker-ha/pull/298)

Add new version to \`TIMESCALEDB_TOOLKIT_EXTENSIONS\` in:
- \`.github/workflows/build_image.yaml\`
- \`.github/workflows/publish_image.yaml\`
- \`Makefile\`

- [ ] hot-forge

[Sample pull request](https://github.com/timescale/hot-forge/pull/67)

Add two new lines to \`bundles.yaml\` containing the new version tag:
\`\`\`
- repository: https://github.com/timescale/timescaledb-toolkit
  tag: $VERSION
\`\`\`

And update \`.github/build_bundles.py\` if new pgrx is required.

- [ ] Copy Changelog.md entries for this release into
  [github release](https://github.com/timescale/timescaledb-toolkit/releases/tag/$VERSION)

- [ ] Update Homebrew

Build binaries on multiple Mac versions/architectures and submit a pull request like
[this example](https://github.com/timescale/homebrew-tap/pull/29/files)
EOF
fi
