diff --git a/Makefile b/Makefile index 4c0794f8..5ac29b7e 100644 --- a/Makefile +++ b/Makefile @@ -34,6 +34,9 @@ update \ update-backend \ update-frontend \ + bump-version \ + prepare-release \ + tag-release \ test \ test-backend \ test-frontend \ @@ -125,6 +128,9 @@ help: @echo " make update Update all dependencies (Go + Flutter)" @echo " make update-backend Update Go dependencies only" @echo " make update-frontend Update Flutter dependencies only" + @echo " make bump-version Bump ./version and frontend/pweb/pubspec.yaml" + @echo " make prepare-release Bump versions and create the release-prep commit for a PR" + @echo " make tag-release Create the local release tag from main after the PR is merged" @echo " make test Run all tests (backend + frontend)" @echo " make test-backend Run Go backend tests only" @echo " make test-frontend Run Flutter tests only" @@ -135,6 +141,15 @@ help: @echo " make logs SERVICE=dev-ledger" @echo " make rebuild SERVICE=dev-ledger" +bump-version: + @./ci/scripts/common/bump_version.sh + +prepare-release: + @./ci/scripts/common/release.sh + +tag-release: + @./ci/scripts/common/tag_release.sh + # First-time initialization init: @echo "$(GREEN)Initializing development environment...$(NC)" diff --git a/README.md b/README.md index f26233c9..8b92d3ef 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,19 @@ make test-frontend # Run Flutter tests only - Tags matching `v*` trigger a full production rebuild and deployment from that exact tagged revision. - Infrastructure workflows for `db` and `nats` remain separately controlled. -Example production release: +Recommended release preparation: + +```bash +./ci/scripts/common/release.sh +# push your branch and open a PR +# merge the PR to main +git checkout main && git pull +# verify the dev deployment from main +./ci/scripts/common/tag_release.sh +git push origin v$(cat version) +``` + +Manual production release from an already-prepared commit: ```bash git tag -a v1.4.0 diff --git a/ci/prod/scripts/bootstrap/network.sh b/ci/prod/scripts/bootstrap/network.sh index 5077f153..57572b2a 100755 --- a/ci/prod/scripts/bootstrap/network.sh +++ b/ci/prod/scripts/bootstrap/network.sh @@ -14,15 +14,28 @@ SSH_OPTS=( -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR - -q ) if [[ "${DEBUG_DEPLOY:-0}" = "1" ]]; then - SSH_OPTS=("${SSH_OPTS[@]/-q/}" -vv) + SSH_OPTS+=(-vv) fi +printf '[bootstrap-shared-network] target=%s network=%s\n' "$REMOTE_TARGET" "$DOCKER_SHARED_NETWORK" >&2 + +set +e +ssh_output="$( ssh "${SSH_OPTS[@]}" "$REMOTE_TARGET" \ - DOCKER_SHARED_NETWORK="$DOCKER_SHARED_NETWORK" bash -s <<'EOSSH' + DOCKER_SHARED_NETWORK="$DOCKER_SHARED_NETWORK" bash -s 2>&1 <<'EOSSH' set -euo pipefail docker network inspect "$DOCKER_SHARED_NETWORK" >/dev/null 2>&1 || \ docker network create "$DOCKER_SHARED_NETWORK" EOSSH +)" +ssh_status=$? +set -e + +if [[ $ssh_status -ne 0 ]]; then + [[ -n "$ssh_output" ]] && printf '%s\n' "$ssh_output" >&2 + exit "$ssh_status" +fi + +[[ -n "$ssh_output" ]] && printf '%s\n' "$ssh_output" diff --git a/ci/scripts/common/bump_version.sh b/ci/scripts/common/bump_version.sh index e126ad66..d74c24b2 100755 --- a/ci/scripts/common/bump_version.sh +++ b/ci/scripts/common/bump_version.sh @@ -1,34 +1,89 @@ -# /bin/bash - -echo "====================================" -echo "Incrementing build version..." -echo "====================================" -VERSION_FILE=./version - -NEW_VERSION=$(cat $VERSION_FILE | awk -F. -v OFS=. 'NF==1{print ++$NF}; NF>1{$NF=sprintf("%0*d", length($NF), ($NF+1)); print}') -echo $NEW_VERSION > $VERSION_FILE - -echo "New version is "$NEW_VERSION - -echo "====================================" -echo "Bumping client version..." -echo "====================================" -FILE="./frontend/mweb/pubspec.yaml" -if sed --version >/dev/null 2>&1; then - # GNU sed - sed -i "s/^version: .*/version: ${NEW_VERSION}+1/" "$FILE" -else - # BSD/macOS sed - sed -i '' -e "s/^version: .*/version: ${NEW_VERSION}+1/" "$FILE" -fi - +#!/usr/bin/env bash set -euo pipefail -# update version file(s) here -# e.g.: ./ci/scripts/common/update_version_file.sh +REPO_ROOT="$(cd "$(dirname "$0")/../../.." && pwd)" +cd "${REPO_ROOT}" -if ! git diff --quiet; then - git add . - git commit -m "chore: bump build version [skip ci]" - git push origin HEAD:main +VERSION_FILE="./version" +PUBSPEC_FILE="./frontend/pweb/pubspec.yaml" +if [[ ! -f "${PUBSPEC_FILE}" && -f "./frontend/pweb/pubspec.yml" ]]; then + PUBSPEC_FILE="./frontend/pweb/pubspec.yml" fi + +if [[ ! -f "${VERSION_FILE}" ]]; then + echo "[bump-version] missing ${VERSION_FILE}" >&2 + exit 1 +fi + +if [[ ! -f "${PUBSPEC_FILE}" ]]; then + echo "[bump-version] missing frontend pubspec file" >&2 + exit 1 +fi + +current_version="$(tr -d '[:space:]' < "${VERSION_FILE}")" +if [[ -z "${current_version}" ]]; then + echo "[bump-version] ${VERSION_FILE} is empty" >&2 + exit 1 +fi + +new_version="$(printf '%s\n' "${current_version}" | awk -F. -v OFS=. ' + NF == 1 { print ++$NF; next } + { $NF = sprintf("%0*d", length($NF), ($NF + 1)); print } +')" + +current_pubspec_version="$( + awk '/^version:[[:space:]]*/ { + sub(/^version:[[:space:]]*/, "", $0) + print + exit + }' "${PUBSPEC_FILE}" +)" + +if [[ -z "${current_pubspec_version}" ]]; then + echo "[bump-version] could not find version line in ${PUBSPEC_FILE}" >&2 + exit 1 +fi + +build_number=0 +if [[ "${current_pubspec_version}" == *+* ]]; then + build_number="${current_pubspec_version##*+}" +fi + +if [[ ! "${build_number}" =~ ^[0-9]+$ ]]; then + echo "[bump-version] invalid build number in ${PUBSPEC_FILE}: ${current_pubspec_version}" >&2 + exit 1 +fi + +next_build_number=$((build_number + 1)) + +version_tmp="${VERSION_FILE}.tmp.$$" +pubspec_tmp="${PUBSPEC_FILE}.tmp.$$" +cleanup() { + rm -f "${version_tmp}" "${pubspec_tmp}" +} +trap cleanup EXIT INT TERM + +printf '%s\n' "${new_version}" > "${version_tmp}" +awk -v version="${new_version}" -v build="${next_build_number}" ' + BEGIN { updated = 0 } + /^version:[[:space:]]*/ && !updated { + print "version: " version "+" build + updated = 1 + next + } + { print } + END { + if (!updated) { + exit 1 + } + } +' "${PUBSPEC_FILE}" > "${pubspec_tmp}" + +mv "${version_tmp}" "${VERSION_FILE}" +mv "${pubspec_tmp}" "${PUBSPEC_FILE}" + +echo "====================================" +echo "Release version bumped" +echo "====================================" +echo "version: ${current_version} -> ${new_version}" +echo "${PUBSPEC_FILE}: ${current_pubspec_version} -> ${new_version}+${next_build_number}" diff --git a/ci/scripts/common/release.sh b/ci/scripts/common/release.sh new file mode 100755 index 00000000..76fe4c7d --- /dev/null +++ b/ci/scripts/common/release.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/../../.." && pwd)" +cd "${REPO_ROOT}" + +VERSION_FILE="./version" +PUBSPEC_FILE="./frontend/pweb/pubspec.yaml" +if [[ ! -f "${PUBSPEC_FILE}" && -f "./frontend/pweb/pubspec.yml" ]]; then + PUBSPEC_FILE="./frontend/pweb/pubspec.yml" +fi + +require_clean_worktree() { + if ! git diff --quiet --ignore-submodules -- || ! git diff --cached --quiet --ignore-submodules --; then + echo "[release] working tree is not clean; commit or stash changes before preparing a release" >&2 + exit 1 + fi +} + +require_clean_worktree + +"${REPO_ROOT}/ci/scripts/common/bump_version.sh" + +new_version="$(tr -d '[:space:]' < "${VERSION_FILE}")" +release_tag="v${new_version}" + +git add "${VERSION_FILE}" "${PUBSPEC_FILE}" +git commit -m "chore: prepare release ${release_tag}" + +echo +echo "Prepared release commit for ${release_tag}" +echo "Next steps:" +echo " 1. push this branch and open a PR" +echo " 2. merge the PR to main" +echo " 3. verify the dev deployment from main" +echo " 4. checkout main and run ./ci/scripts/common/tag_release.sh" diff --git a/ci/scripts/common/tag_release.sh b/ci/scripts/common/tag_release.sh new file mode 100755 index 00000000..cbf42d3e --- /dev/null +++ b/ci/scripts/common/tag_release.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/../../.." && pwd)" +cd "${REPO_ROOT}" + +VERSION_FILE="./version" + +require_clean_worktree() { + if ! git diff --quiet --ignore-submodules -- || ! git diff --cached --quiet --ignore-submodules --; then + echo "[tag-release] working tree is not clean; commit or stash changes before tagging a release" >&2 + exit 1 + fi +} + +require_clean_worktree + +current_branch="$(git symbolic-ref --short -q HEAD || true)" +if [[ -n "${current_branch}" && "${current_branch}" != "main" ]]; then + echo "[tag-release] checkout main before creating the release tag" >&2 + exit 1 +fi + +if [[ ! -f "${VERSION_FILE}" ]]; then + echo "[tag-release] missing ${VERSION_FILE}" >&2 + exit 1 +fi + +release_version="$(tr -d '[:space:]' < "${VERSION_FILE}")" +if [[ -z "${release_version}" ]]; then + echo "[tag-release] ${VERSION_FILE} is empty" >&2 + exit 1 +fi + +release_tag="v${release_version}" +if git rev-parse -q --verify "refs/tags/${release_tag}" >/dev/null; then + echo "[tag-release] tag ${release_tag} already exists locally" >&2 + exit 1 +fi + +git tag -a "${release_tag}" -m "Release ${release_tag}" + +echo "Created release tag ${release_tag} at $(git rev-parse --short HEAD)" +echo "Next step:" +echo " git push origin ${release_tag}"