feat: auto-release on PR merge with required release labels

- Add CI check that requires a release label (release:patch, release:minor,
  release:major, or no-release) on PRs that change publishable packages
- Add workflow that triggers release-bump-version on merge based on label
- Cascade dedup prevents double-bumping (e.g., core change won't also
  separately bump computer/agent since the cascade handles it)
This commit is contained in:
f-trycua
2026-02-09 17:59:38 -08:00
parent 72b20df8d4
commit 50061766ed
2 changed files with 223 additions and 0 deletions

View File

@@ -0,0 +1,96 @@
name: "CI: Require Release Label"
on:
pull_request:
types: [opened, synchronize, labeled, unlabeled]
jobs:
check-release-label:
runs-on: ubuntu-latest
steps:
- name: Check for release label on publishable changes
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Get changed files in this PR
CHANGED_FILES=$(gh pr diff "${{ github.event.pull_request.number }}" \
--repo "${{ github.repository }}" --name-only 2>/dev/null || true)
if [ -z "$CHANGED_FILES" ]; then
echo "No changed files detected, skipping."
exit 0
fi
# Map paths to publishable services
declare -A PATH_TO_SERVICE=(
["libs/python/cua-cli/"]="pypi/cli"
["libs/python/agent/"]="pypi/agent"
["libs/python/computer/"]="pypi/computer"
["libs/python/core/"]="pypi/core"
["libs/python/som/"]="pypi/som"
["libs/python/bench-ui/"]="pypi/bench-ui"
["libs/cua-bench/"]="pypi/bench"
["libs/python/computer-server/"]="pypi/computer-server"
["libs/python/mcp-server/"]="pypi/mcp-server"
["libs/typescript/cua-cli/"]="npm/cli"
["libs/typescript/computer/"]="npm/computer"
["libs/typescript/core/"]="npm/core"
["libs/typescript/playground/"]="npm/playground"
["libs/cuabot/"]="npm/cuabot"
["libs/xfce/"]="docker/xfce"
["libs/kasm/"]="docker/kasm"
["libs/lumier/"]="docker/lumier"
["libs/qemu-docker/android/"]="docker/qemu-android"
["libs/qemu-docker/linux/"]="docker/qemu-linux"
["libs/qemu-docker/windows/"]="docker/qemu-windows"
["libs/lume/"]="lume"
)
# Find affected services
AFFECTED=""
for path_prefix in "${!PATH_TO_SERVICE[@]}"; do
if echo "$CHANGED_FILES" | grep -q "^${path_prefix}"; then
service="${PATH_TO_SERVICE[$path_prefix]}"
AFFECTED="${AFFECTED}${service}\n"
fi
done
AFFECTED=$(echo -e "$AFFECTED" | sort -u | sed '/^$/d')
if [ -z "$AFFECTED" ]; then
echo "No publishable packages affected, skipping."
exit 0
fi
echo "Affected packages:"
echo "$AFFECTED" | while read -r svc; do echo " - $svc"; done
# Check for release label
LABELS='${{ toJSON(github.event.pull_request.labels.*.name) }}'
HAS_RELEASE_LABEL=false
for label in "release:patch" "release:minor" "release:major" "no-release"; do
if echo "$LABELS" | grep -q "\"${label}\""; then
HAS_RELEASE_LABEL=true
echo "Found label: $label"
break
fi
done
if [ "$HAS_RELEASE_LABEL" = "false" ]; then
echo ""
echo "=========================================="
echo "ERROR: Missing release label!"
echo "=========================================="
echo ""
echo "This PR changes the following publishable packages:"
echo "$AFFECTED" | while read -r svc; do echo " - $svc"; done
echo ""
echo "Please add one of these labels to the PR:"
echo " release:patch - bump patch version (bug fixes)"
echo " release:minor - bump minor version (new features)"
echo " release:major - bump major version (breaking changes)"
echo " no-release - skip publishing (e.g., tests/docs only)"
echo ""
exit 1
fi

127
.github/workflows/release-on-merge.yml vendored Normal file
View File

@@ -0,0 +1,127 @@
name: "CD: Auto Release on Merge"
on:
pull_request:
types: [closed]
jobs:
auto-release:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- name: Detect packages and trigger release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PR_NUMBER="${{ github.event.pull_request.number }}"
REPO="${{ github.repository }}"
# Check labels
LABELS='${{ toJSON(github.event.pull_request.labels.*.name) }}'
# Determine bump type from label
BUMP_TYPE=""
if echo "$LABELS" | grep -q '"no-release"'; then
echo "no-release label found, skipping."
exit 0
elif echo "$LABELS" | grep -q '"release:major"'; then
BUMP_TYPE="major"
elif echo "$LABELS" | grep -q '"release:minor"'; then
BUMP_TYPE="minor"
elif echo "$LABELS" | grep -q '"release:patch"'; then
BUMP_TYPE="patch"
else
echo "No release label found, skipping."
exit 0
fi
echo "Bump type: $BUMP_TYPE"
# Get changed files
CHANGED_FILES=$(gh pr diff "$PR_NUMBER" --repo "$REPO" --name-only 2>/dev/null || true)
if [ -z "$CHANGED_FILES" ]; then
echo "No changed files, skipping."
exit 0
fi
# Map paths to services
declare -A PATH_TO_SERVICE=(
["libs/python/cua-cli/"]="pypi/cli"
["libs/python/agent/"]="pypi/agent"
["libs/python/computer/"]="pypi/computer"
["libs/python/core/"]="pypi/core"
["libs/python/som/"]="pypi/som"
["libs/python/bench-ui/"]="pypi/bench-ui"
["libs/cua-bench/"]="pypi/bench"
["libs/python/computer-server/"]="pypi/computer-server"
["libs/python/mcp-server/"]="pypi/mcp-server"
["libs/typescript/cua-cli/"]="npm/cli"
["libs/typescript/computer/"]="npm/computer"
["libs/typescript/core/"]="npm/core"
["libs/typescript/playground/"]="npm/playground"
["libs/cuabot/"]="npm/cuabot"
["libs/xfce/"]="docker/xfce"
["libs/kasm/"]="docker/kasm"
["libs/lumier/"]="docker/lumier"
["libs/qemu-docker/android/"]="docker/qemu-android"
["libs/qemu-docker/linux/"]="docker/qemu-linux"
["libs/qemu-docker/windows/"]="docker/qemu-windows"
["libs/lume/"]="lume"
)
# Find affected services
declare -A AFFECTED_MAP
for path_prefix in "${!PATH_TO_SERVICE[@]}"; do
if echo "$CHANGED_FILES" | grep -q "^${path_prefix}"; then
service="${PATH_TO_SERVICE[$path_prefix]}"
AFFECTED_MAP["$service"]=1
fi
done
if [ ${#AFFECTED_MAP[@]} -eq 0 ]; then
echo "No publishable packages affected, skipping."
exit 0
fi
echo "Affected packages before dedup:"
for svc in "${!AFFECTED_MAP[@]}"; do echo " - $svc"; done
# Cascade dedup: remove services already covered by upstream cascades
# pypi/core cascades to pypi/computer and pypi/agent
if [[ -n "${AFFECTED_MAP["pypi/core"]}" ]]; then
unset AFFECTED_MAP["pypi/computer"]
unset AFFECTED_MAP["pypi/agent"]
fi
# pypi/computer cascades to pypi/agent
if [[ -n "${AFFECTED_MAP["pypi/computer"]}" ]]; then
unset AFFECTED_MAP["pypi/agent"]
fi
# pypi/som cascades to pypi/agent
if [[ -n "${AFFECTED_MAP["pypi/som"]}" ]]; then
unset AFFECTED_MAP["pypi/agent"]
fi
# npm/core cascades to npm/computer
if [[ -n "${AFFECTED_MAP["npm/core"]}" ]]; then
unset AFFECTED_MAP["npm/computer"]
fi
echo ""
echo "Services to release (after cascade dedup):"
for svc in "${!AFFECTED_MAP[@]}"; do echo " - $svc"; done
# Trigger release-bump-version for each affected service
for svc in "${!AFFECTED_MAP[@]}"; do
echo ""
echo "Triggering release-bump-version for: $svc ($BUMP_TYPE)"
gh workflow run release-bump-version.yml \
--repo "$REPO" \
-f service="$svc" \
-f bump_type="$BUMP_TYPE"
echo "Triggered successfully."
# Sleep between triggers to avoid race conditions on main branch
sleep 15
done
echo ""
echo "All releases triggered!"