#!/usr/bin/env bash set -eo pipefail [[ $TRACE ]] && set -x readonly ROOT_DIR="$(cd "$(dirname "$(dirname "${BASH_SOURCE[0]}")")" && pwd)" readonly TMP_WORK_DIR="$(mktemp -d "/tmp/dokku-release.XXXXXX")" readonly DOKKU_GIT_REV="$(git rev-parse HEAD)" trap "rm -rf '$TMP_WORK_DIR' >/dev/null" RETURN INT TERM EXIT log-info() { # shellcheck disable=SC2034 declare desc="Log info formatter" echo "$*" } log-error() { # shellcheck disable=SC2034 declare desc="Log error formatter" echo "! $*" 1>&2 } log-fail() { # shellcheck disable=SC2034 declare desc="Log fail formatter" log-error "$*" exit 1 } fn-build-dokku() { declare desc="Builds dokku packages within a docker container" declare IS_RELEASE="$1" DOKKU_VERSION="$2" local NAME GOLANG_VERSION if [[ -z "$DOKKU_VERSION" ]]; then log-error "Invalid deb file specified" return 1 fi NAME="dokku:build" [[ "$IS_RELEASE" == "true" ]] && NAME="dokku:build-release" [[ -n "$DOKKU_VERSION" ]] || DOKKU_VERSION="master" pushd "$ROOT_DIR" >/dev/null GOLANG_VERSION="$(grep BUILD_IMAGE common.mk | cut -d' ' -f3 | cut -d':' -f2)" docker buildx build \ --platform linux/arm/v7,linux/arm64/v8,linux/amd64 \ --build-arg DOKKU_VERSION="$DOKKU_VERSION" \ --build-arg DOKKU_GIT_REV="$DOKKU_GIT_REV" \ --build-arg IS_RELEASE="$IS_RELEASE" \ --build-arg PLUGIN_MAKE_TARGET="build" \ --build-arg GOLANG_VERSION="$GOLANG_VERSION" \ --progress plain \ -f "contrib/build-dokku.Dockerfile" \ -t "$NAME" . return "$?" } fn-extract-package() { declare desc="Extract packages from a docker container to the root directory" declare IS_RELEASE="$1" PACKAGE_NAME="$2" local NAME if [[ -z "$PACKAGE_NAME" ]]; then log-error "Invalid deb file specified" return 1 fi NAME="dokku:build" [[ "$IS_RELEASE" == "true" ]] && NAME="dokku:build-release" log-info "(extract-package) writing ${PACKAGE_NAME} to correct path" docker run --rm --entrypoint cat "$NAME" "/tmp/${PACKAGE_NAME}" >"${ROOT_DIR}/build/${PACKAGE_NAME}" return "$?" } fn-in-array() { declare desc="return true if value ($1) is in list (all other arguments)" local e for e in "${@:2}"; do [[ "$e" == "$1" ]] && return 0 done return 1 } fn-is-release() { declare desc="Checks if a given run is a release run" declare RELEASE="$1" local IS_RELEASE=false if [[ "$RELEASE" == "major" ]] || [[ "$RELEASE" == "minor" ]] || [[ "$RELEASE" == "patch" ]]; then IS_RELEASE=true fi echo "$IS_RELEASE" } fn-version-current() { declare desc="Retrieves the current version of dokku" pushd "$ROOT_DIR" >/dev/null grep Version debian/control | cut -d' ' -f2 } fn-version-next() { declare desc="Calculates the next version name of dokku" declare CURRENT_VERSION="$1" RELEASE="$2" local major minor patch short_object major=$(echo "$CURRENT_VERSION" | awk '{split($0,a,"."); print a[1]}') minor=$(echo "$CURRENT_VERSION" | awk '{split($0,a,"."); print a[2]}') patch=$(echo "$CURRENT_VERSION" | awk '{split($0,a,"."); print a[3]}') if [[ "$RELEASE" == "major" ]]; then major=$((major + 1)) minor="0" patch="0" elif [[ "$RELEASE" == "minor" ]]; then minor=$((minor + 1)) patch="0" elif [[ "$RELEASE" == "patch" ]]; then patch=$((patch + 1)) else short_object=$(git log --oneline | tail -n 1 | cut -d' ' -f1) echo "${CURRENT_VERSION}build+$(echo "$DOKKU_GIT_REV" | cut -c1-${#short_object})" return 0 fi echo "${major}.${minor}.${patch}" } fn-publish-package() { declare desc="Publishes a package to packagecloud" declare IS_RELEASE="$1" RELEASE_TYPE="$2" PACKAGE_NAME="$3" local REPOSITORY=dokku/dokku-betafish DIST=ubuntu EXIT_CODE=0 local OS_ID ex [[ "$IS_RELEASE" == "true" ]] && REPOSITORY=dokku/dokku [[ "$RELEASE_TYPE" == "rpm" ]] && DIST=el/7 [[ "$RELEASE_TYPE" == "raspbian" ]] && DIST=raspbian if [[ "$RELEASE_TYPE" == "raspbian" ]]; then OS_IDS=("buster") for OS_ID in "${OS_IDS[@]}"; do log-info "(release-dokku) pushing deb to packagecloud.com/${REPOSITORY}/${DIST}" package_cloud push "${REPOSITORY}/${DIST}/${OS_ID}" "$PACKAGE_NAME" ex="$?" if [[ "$ex" -ne "0" ]]; then EXIT_CODE="$ex" fi done elif [[ "$DIST" == "ubuntu" ]]; then OS_IDS=("bionic" "focal") for OS_ID in "${OS_IDS[@]}"; do log-info "(release-dokku) pushing ${RELEASE_TYPE} to packagecloud.com/${REPOSITORY}/${DIST}" package_cloud push "${REPOSITORY}/${DIST}/${OS_ID}" "$PACKAGE_NAME" ex="$?" if [[ "$ex" -ne "0" ]]; then EXIT_CODE="$ex" fi done DIST=debian OS_IDS=("stretch" "buster" "bullseye") for OS_ID in "${OS_IDS[@]}"; do log-info "(release-dokku) pushing ${RELEASE_TYPE} to packagecloud.com/${REPOSITORY}/${DIST}" package_cloud push "${REPOSITORY}/${DIST}/${OS_ID}" "$PACKAGE_NAME" ex="$?" if [[ "$ex" -ne "0" ]]; then EXIT_CODE="$ex" fi done else log-info "(release-dokku) pushing ${RELEASE_TYPE} to packagecloud.com/${REPOSITORY}/${DIST}" package_cloud push "${REPOSITORY}/${DIST}" "$PACKAGE_NAME" EXIT_CODE="$?" fi return "$EXIT_CODE" } fn-replace-version() { declare desc="Replaces the version within a file" local CURRENT_VERSION="$1" local NEXT_VERSION="$2" local FILENAME="$3" sed -i.bak "s/${CURRENT_VERSION//./\.}/$NEXT_VERSION/g" "$FILENAME" && rm "$FILENAME.bak" git add "$FILENAME" } fn-repo-merged-requests() { declare desc="Retrieves all pull request ids since a given release of dokku" declare TAG="$1" git log "v${TAG}..HEAD" --oneline | grep "Merge pull request" | cut -d' ' -f 5 | cut -d '#' -f2 } fn-repo-push-tags() { declare desc="Pushes tags and the repository to github" declare IS_RELEASE="$1" if [[ "$IS_RELEASE" == "false" ]]; then return fi pushd "$ROOT_DIR" >/dev/null git push --tags origin master } fn-repo-update() { declare desc="Updates files within the repository for a given release" declare IS_RELEASE="$1" CURRENT_VERSION="$2" NEXT_VERSION="$3" log-info "(release-dokku) Replacing ${CURRENT_VERSION} with ${NEXT_VERSION}" for f in plugins/*/plugin.toml; do fn-replace-version "$CURRENT_VERSION" "$NEXT_VERSION" "$f" done if [[ "$IS_RELEASE" == "false" ]]; then return fi fn-replace-version "$CURRENT_VERSION" "$NEXT_VERSION" debian/control fn-replace-version "$CURRENT_VERSION" "$NEXT_VERSION" Dockerfile fn-replace-version "$CURRENT_VERSION" "$NEXT_VERSION" docs/advanced-usage/plugin-management.md fn-replace-version "$CURRENT_VERSION" "$NEXT_VERSION" docs/assets/favicons/browserconfig.xml fn-replace-version "$CURRENT_VERSION" "$NEXT_VERSION" docs/assets/favicons/manifest.json fn-replace-version "$CURRENT_VERSION" "$NEXT_VERSION" docs/assets/style.css fn-replace-version "$CURRENT_VERSION" "$NEXT_VERSION" docs/development/release-process.md fn-replace-version "$CURRENT_VERSION" "$NEXT_VERSION" docs/getting-started/install/docker.md fn-replace-version "$CURRENT_VERSION" "$NEXT_VERSION" docs/getting-started/installation.md fn-replace-version "$CURRENT_VERSION" "$NEXT_VERSION" docs/home.html fn-replace-version "$CURRENT_VERSION" "$NEXT_VERSION" docs/template.html fn-replace-version "$CURRENT_VERSION" "$NEXT_VERSION" README.md if [[ "$RELEASE" == 'patch' ]]; then fn-replace-version "$CURRENT_VERSION" "$NEXT_VERSION" docs/assets/versions.json else versions=$(python -c 'import json,sys;d=json.load(sys.stdin);d["max-versions"].append("'"${NEXT_VERSION}"'"); print(json.dumps(d, indent=2, sort_keys=True))' docs/assets/versions.json git add docs/assets/versions.json fi if [[ "$IS_RELEASE" == "false" ]]; then return fi log-info "(release-dokku) Adding HISTORY.md, committing and tagging" fn-repo-update-history-and-commit "$CURRENT_VERSION" "$NEXT_VERSION" git tag "v${NEXT_VERSION}" } fn-repo-update-history-and-commit() { declare desc="Updates the history file and commits the changes" declare CURRENT_VERSION="$1" NEXT_VERSION="$2" local PULL_REQUEST_ID TITLE AUTHOR TYPE CHANGELOG_TEXT local COMMIT_MESSAGE HISTORY HISTORY_CONTENTS HISTORY_DOCUMENTATION HISTORY_ENHANCEMENT HISTORY_BC_BREAK HISTORY_REFACTOR HISTORY_BUG HISTORY_OTHER HISTORY_TESTS ISSUE_FILE pushd "$ROOT_DIR" >/dev/null mkdir -p /tmp/github-data for PULL_REQUEST_ID in $(fn-repo-merged-requests "$CURRENT_VERSION"); do ISSUE_FILE="/tmp/github-data/${PULL_REQUEST_ID}.json" if [[ ! -f "$ISSUE_FILE" ]]; then curl -sS "https://api.github.com/repos/dokku/dokku/issues/${PULL_REQUEST_ID}" >"$ISSUE_FILE" fi TITLE="$(jq -r '.title' "$ISSUE_FILE")" AUTHOR="$(jq -r '.user.login' "$ISSUE_FILE")" TYPE="$(jq -r '.labels[] | select( .name | contains("type") ) | .name' "$ISSUE_FILE" | cut -d' ' -f2)" CHANGELOG_TEXT="- #${PULL_REQUEST_ID}: @${AUTHOR} ${TITLE}" if [[ "$TYPE" == "bc-break" ]]; then HISTORY_BC_BREAK="${HISTORY_BC_BREAK}"$'\n'"$CHANGELOG_TEXT" elif [[ "$TYPE" == "bug" ]]; then HISTORY_BUG="${HISTORY_BUG}"$'\n'"$CHANGELOG_TEXT" elif [[ "$TYPE" == "documentation" ]]; then HISTORY_DOCUMENTATION="${HISTORY_DOCUMENTATION}"$'\n'"$CHANGELOG_TEXT" elif [[ "$TYPE" == "enhancement" ]]; then HISTORY_ENHANCEMENT="${HISTORY_ENHANCEMENT}"$'\n'"$CHANGELOG_TEXT" elif [[ "$TYPE" == "refactor" ]]; then HISTORY_REFACTOR="${HISTORY_REFACTOR}"$'\n'"$CHANGELOG_TEXT" elif [[ "$TYPE" == "tests" ]]; then HISTORY_TESTS="${HISTORY_TESTS}"$'\n'"$CHANGELOG_TEXT" else HISTORY_OTHER="${HISTORY_OTHER}"$'\n'"$CHANGELOG_TEXT" fi done HISTORY="# History" HISTORY="${HISTORY}"$'\n\n'"## ${NEXT_VERSION}" HISTORY="${HISTORY}"$'\n\n'"Install/update via the bootstrap script:" HISTORY="${HISTORY}"$'\n\n'"\`\`\`shell" HISTORY="${HISTORY}"$'\n'"wget https://raw.githubusercontent.com/dokku/dokku/v${NEXT_VERSION}/bootstrap.sh" HISTORY="${HISTORY}"$'\n'"sudo DOKKU_TAG=v${NEXT_VERSION} bash bootstrap.sh" HISTORY="${HISTORY}"$'\n'"\`\`\`" if [[ -f "docs/appendices/${NEXT_VERSION}-migration-guide.md" ]]; then HISTORY="${HISTORY}"$'\n\n'"See the [${NEXT_VERSION} migration guide](/docs/appendices/${NEXT_VERSION}-migration-guide.md) for more information on migrating to ${NEXT_VERSION}." fi if [[ "$HISTORY_BC_BREAK" ]]; then HISTORY="${HISTORY}"$'\n\n'"### Backwards Compatibility Breaks" HISTORY="${HISTORY}"$'\n'"$HISTORY_BC_BREAK" fi if [[ "$HISTORY_BUG" ]]; then HISTORY="${HISTORY}"$'\n\n'"### Bug Fixes" HISTORY="${HISTORY}"$'\n'"$HISTORY_BUG" fi if [[ "$HISTORY_ENHANCEMENT" ]]; then HISTORY="${HISTORY}"$'\n\n'"### New Features" HISTORY="${HISTORY}"$'\n'"$HISTORY_ENHANCEMENT" fi if [[ "$HISTORY_REFACTOR" ]]; then HISTORY="${HISTORY}"$'\n\n'"### Refactors" HISTORY="${HISTORY}"$'\n'"$HISTORY_REFACTOR" fi if [[ "$HISTORY_DOCUMENTATION" ]]; then HISTORY="${HISTORY}"$'\n\n'"### Documentation" HISTORY="${HISTORY}"$'\n'"$HISTORY_DOCUMENTATION" fi if [[ "$HISTORY_TESTS" ]]; then HISTORY="${HISTORY}"$'\n\n'"### Tests" HISTORY="${HISTORY}"$'\n'"$HISTORY_TESTS" fi if [[ "$HISTORY_OTHER" ]]; then HISTORY="${HISTORY}"$'\n\n'"### Other" HISTORY="${HISTORY}"$'\n'"$HISTORY_OTHER" fi sed -i.bak '1d' HISTORY.md && rm HISTORY.md.bak HISTORY_CONTENTS="$(cat HISTORY.md)" echo -e "$HISTORY\n$HISTORY_CONTENTS" >HISTORY.md git add HISTORY.md COMMIT_MESSAGE="Release ${NEXT_VERSION}"$'\n\n'"${HISTORY}" git commit -m "${COMMIT_MESSAGE}" } fn-require-bin() { declare desc="Checks that a binary exists" declare BINARY="$1" if ! command -v "$BINARY" &>/dev/null; then log-fail "Missing ${BINARY}, please install it" fi } fn-build-docker-image() { declare desc="Builds the dokku docker image and tags it with the given version" declare VERSION="$1" docker build -t "dokku/dokku:$VERSION" . } main() { declare RELEASE="$1" local CURRENT_VERSION NEXT_VERSION IS_RELEASE local VALID_RELEASE_LEVELS=("major" "minor" "patch" "betafish" "build") if [[ "$RELEASE" == '--trace' ]]; then shift 1 RELEASE="$1" TRACE=1 && set -x fi if [[ -z "$RELEASE" ]]; then log-fail "Argument 1 must be one of [major, minor, patch, betafish, build], none given" fi if ! fn-in-array "$RELEASE" "${VALID_RELEASE_LEVELS[@]}"; then log-fail "Argument 1 must be one of [major, minor, patch, betafish, build], '${RELEASE}' given" fi log-info "Creating $RELEASE release" mkdir -p "build" fn-require-bin "docker" fn-require-bin "git" if [[ "$RELEASE" != "build" ]]; then fn-require-bin "package_cloud" [[ -n "$PACKAGECLOUD_TOKEN" ]] || log-fail "Missing PACKAGECLOUD_TOKEN environment variable" fi CURRENT_VERSION="$(fn-version-current)" NEXT_VERSION="$(fn-version-next "$CURRENT_VERSION" "$RELEASE")" IS_RELEASE="$(fn-is-release "$RELEASE")" echo "v${NEXT_VERSION}" >build/next-version fn-repo-update "$IS_RELEASE" "$CURRENT_VERSION" "$NEXT_VERSION" fn-build-dokku "$IS_RELEASE" "$NEXT_VERSION" || log-fail "Error building package" fn-extract-package "$IS_RELEASE" "dokku_${NEXT_VERSION}_armhf.deb" || log-fail "Error extracting deb package" fn-extract-package "$IS_RELEASE" "dokku_${NEXT_VERSION}_arm64.deb" || log-fail "Error extracting deb package" fn-extract-package "$IS_RELEASE" "dokku_${NEXT_VERSION}_amd64.deb" || log-fail "Error extracting deb package" fn-extract-package "$IS_RELEASE" "dokku-${NEXT_VERSION}-1.x86_64.rpm" || log-fail "Error extracting rpm package" mkdir -p build/package cp -f "build/dokku_${NEXT_VERSION}_amd64.deb" build/package/dokku-amd64.deb cp -f "build/dokku_${NEXT_VERSION}_arm64.deb" build/package/dokku-arm64.deb cp -f "build/dokku_${NEXT_VERSION}_armhf.deb" build/package/dokku-armhf.deb cp -f "build/dokku-${NEXT_VERSION}-1.x86_64.rpm" build/package/dokku-amd64.rpm if [[ "$RELEASE" != "build" ]]; then fn-publish-package "$IS_RELEASE" "raspbian" "build/dokku_${NEXT_VERSION}_armhf.deb" || log-fail "Error publishing deb package" fn-publish-package "$IS_RELEASE" "deb" "build/dokku_${NEXT_VERSION}_arm64.deb" || log-fail "Error publishing deb package" fn-publish-package "$IS_RELEASE" "deb" "build/dokku_${NEXT_VERSION}_amd64.deb" || log-fail "Error publishing deb package" fn-publish-package "$IS_RELEASE" "rpm" "build/dokku-${NEXT_VERSION}-1.x86_64.rpm" || log-fail "Error publishing rpm package" fn-build-docker-image "$NEXT_VERSION" || log-fail "Error building docker image" if [[ "$IS_RELEASE" == "true" ]]; then docker tag "dokku/dokku:$NEXT_VERSION" "dokku/dokku:latest" docker push "dokku/dokku:$NEXT_VERSION" docker push "dokku/dokku:latest" fi fn-repo-push-tags "$IS_RELEASE" fi if [[ "$IS_RELEASE" != "true" ]]; then git reset -q HEAD plugins/*/plugin.toml git checkout -- plugins/*/plugin.toml fi } main "$@"