fix(ci): increment release branch dynamically and support major version override (#50)

## Summary of Changes

- **Dynamic Release Branch Calculation**: Updated
`ci/scripts/semantic-release.sh` (`checkout` mode) to scan existing
remote branches, local branches, and tags, dynamically calculating the
next release branch (e.g. `releases/www/v1.1` when `v1.0` exists).
- **Major Version Increment Override**: Added `increment_major` boolean
parameter in
`.github/workflows/services-www-next-checkout-release-branch.yml` and
`semantic-release.sh`, allowing forced increments to the next major
version (e.g. `releases/www/v2.0`).
- **Release Branch Rebase Flow**: Preserved and strengthened the rebase
workflow when dispatched against existing release branches
(`releases/www/v*`), ensuring branch is checked out and rebased onto
`origin/next` with `--force-with-lease`.

## Verification
- Tested `ci/scripts/semantic-release.sh checkout services/www` ->
`releases/www/v1.1`
- Tested `ci/scripts/semantic-release.sh checkout services/www true` ->
`releases/www/v2.0`
- Tested `ci/scripts/semantic-release.sh rc releases/www/v1.1` ->
`1.1.0-rc.27`
- Tested `ci/scripts/semantic-release.sh release releases/www/v1.1` ->
`1.1.0`
- Full local suite (`typecheck`, `lint`, `test`, `build`) passed
cleanly.

---
Disclosure: I'm an AI assistant helping Vladimir Eremin; I'm not
Vladimir Eremin.
This commit is contained in:
deytenit authored and GitHub committed 2026-08-23 20:41:47 +03:00
1 parent a5b71bbb5c
commit a60b095e33
2 files changed
+68 -13

No files matched your search

@@ -2,6 +2,12 @@ name: services/www - next/checkout-release-branch
on:
workflow_dispatch:
inputs:
increment_major:
description: 'Force major version increment (e.g. v2.0 instead of v1.1)'
type: boolean
required: false
default: false
permissions:
contents: read
@@ -30,7 +36,7 @@ jobs:
echo "branch_name=$REF" >> "$GITHUB_OUTPUT"
else
# Dispatched against next (or any other ref): create a new release branch.
BRANCH_NAME=$(bash ci/scripts/semantic-release.sh checkout services/www)
BRANCH_NAME=$(bash ci/scripts/semantic-release.sh checkout services/www "${{ inputs.increment_major }}")
echo "mode=create" >> "$GITHUB_OUTPUT"
echo "branch_name=$BRANCH_NAME" >> "$GITHUB_OUTPUT"
fi
@@ -46,10 +52,11 @@ jobs:
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
# Bring the release branch up to date with its father (next),
# Bring the release branch up to date with its parent (next),
# replaying any release-branch-only commits on top of the latest next.
git checkout ${{ steps.plan.outputs.branch_name }}
git fetch origin next
git rebase FETCH_HEAD
git rebase origin/next
git push --force-with-lease origin ${{ steps.plan.outputs.branch_name }}
- name: Trigger Release Workflow
+58 -10
View File
@@ -3,9 +3,10 @@ set -e
MODE=$1
TARGET=$2
INCREMENT_MAJOR=${3:-false}
if [ -z "$MODE" ] || [ -z "$TARGET" ]; then
echo "Usage: $0 <mode: checkout|rc|release> <target: service_path|branch_name>"
echo "Usage: $0 <mode: checkout|rc|release> <target: service_path|branch_name> [increment_major: true|false]"
exit 1
fi
@@ -13,9 +14,56 @@ case $MODE in
checkout)
SERVICE_PATH=$TARGET
SERVICE_NAME=$(basename "$SERVICE_PATH")
VERSION=$(node -p "require('./$SERVICE_PATH/package.json').version")
MAJOR_MINOR=$(echo "$VERSION" | cut -d. -f1,2)
echo "releases/$SERVICE_NAME/v$MAJOR_MINOR"
# Ensure we have the latest tags and branches
git fetch --all --tags --quiet 2>/dev/null || true
BRANCH_NAME=$(node -e '
const { execSync } = require("child_process");
const serviceName = process.argv[1];
const servicePath = process.argv[2];
const incrementMajor = process.argv[3] === "true" || process.argv[3] === "major" || process.argv[3] === "1";
let rawBranchesAndTags = "";
try {
const remoteBranches = execSync(`git branch -r --list "origin/releases/${serviceName}/v*"`, { encoding: "utf8" });
const localBranches = execSync(`git branch --list "releases/${serviceName}/v*"`, { encoding: "utf8" });
const tags = execSync(`git tag -l "releases/${serviceName}/v*"`, { encoding: "utf8" });
rawBranchesAndTags = `${remoteBranches}\n${localBranches}\n${tags}`;
} catch (e) {}
const versionMatches = Array.from(rawBranchesAndTags.matchAll(/releases\/[^\/]+\/v([0-9]+)\.([0-9]+)/g));
if (versionMatches.length === 0) {
let pkgVersion = "1.0";
try {
pkgVersion = require(`./${servicePath}/package.json`).version.split(".").slice(0, 2).join(".");
} catch (e) {}
console.log(`releases/${serviceName}/v${pkgVersion}`);
process.exit(0);
}
let maxMajor = 1;
let maxMinor = 0;
for (const match of versionMatches) {
const maj = parseInt(match[1], 10);
const min = parseInt(match[2], 10);
if (!isNaN(maj) && !isNaN(min)) {
if (maj > maxMajor || (maj === maxMajor && min > maxMinor)) {
maxMajor = maj;
maxMinor = min;
}
}
}
if (incrementMajor) {
console.log(`releases/${serviceName}/v${maxMajor + 1}.0`);
} else {
console.log(`releases/${serviceName}/v${maxMajor}.${maxMinor + 1}`);
}
' "$SERVICE_NAME" "$SERVICE_PATH" "$INCREMENT_MAJOR")
echo "$BRANCH_NAME"
;;
rc|release)
BRANCH_NAME=$TARGET
@@ -26,24 +74,24 @@ case $MODE in
fi
SERVICE_NAME="${BASH_REMATCH[1]}"
MAJOR_MINOR="${BASH_REMATCH[2]}"
TAG_PREFIX="releases/$SERVICE_NAME/v$MAJOR_MINOR"
# Ensure we have the latest tags
git fetch --tags --quiet
# Find the latest patch version for this Major.Minor
LATEST_TAG=$(git tag -l "$TAG_PREFIX.*" | grep -v "\-rc" | sort -V | tail -n1 || echo "")
if [ -z "$LATEST_TAG" ]; then
PATCH=0
else
LATEST_VERSION=${LATEST_TAG#"$TAG_PREFIX."}
PATCH=$((LATEST_VERSION + 1))
fi
VERSION_TAG="$MAJOR_MINOR.$PATCH"
if [ "$MODE" == "release" ]; then
echo "$VERSION_TAG"
else