Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 144 additions & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
name: Release

concurrency:
group: release-main
cancel-in-progress: false

on:
workflow_dispatch:
inputs:
Expand All @@ -19,13 +23,25 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
outputs:
new_version: ${{ steps.compute.outputs.new_version }}
tag: ${{ steps.tag.outputs.tag }}
bump_branch: ${{ steps.bump_branch.outputs.name }}
steps:
- name: Ensure workflow is running on main
shell: bash
run: |
set -euo pipefail
if [[ "${GITHUB_REF_NAME}" != "main" ]]; then
echo "This workflow must be run on the main branch. Current ref: ${GITHUB_REF_NAME}" >&2
exit 1
fi

- name: Checkout repository
uses: actions/checkout@v6
with:
ref: main
fetch-depth: 0

- name: Compute new version
Expand Down Expand Up @@ -78,48 +94,170 @@ jobs:
echo "Updating all version references to $NEW_VERSION"
python3 tools/update_versions.py --version "$NEW_VERSION"

- name: Commit and push changes
- name: Commit version bump to a temporary branch
id: bump_branch
env:
NEW_VERSION: ${{ steps.compute.outputs.new_version }}
shell: bash
run: |
set -euo pipefail
BRANCH="release/v${NEW_VERSION}"
echo "name=$BRANCH" >> "$GITHUB_OUTPUT"

git config user.name "GitHub Actions"
git config user.email "actions@github.com"
git checkout -b "$BRANCH"
git add MCPForUnity/package.json manifest.json "Server/pyproject.toml" Server/README.md README.md docs/i18n/README-zh.md
if git diff --cached --quiet; then
echo "No version changes to commit."
else
git commit -m "chore: bump version to ${NEW_VERSION}"
fi

BRANCH="${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}}"
echo "Pushing to branch: $BRANCH"
echo "Pushing bump branch $BRANCH"
git push origin "$BRANCH"

- name: Create and push tag
- name: Create PR for version bump into main
id: bump_pr
env:
GH_TOKEN: ${{ github.token }}
NEW_VERSION: ${{ steps.compute.outputs.new_version }}
BRANCH: ${{ steps.bump_branch.outputs.name }}
shell: bash
run: |
set -euo pipefail
PR_URL=$(gh pr create \
--base main \
--head "$BRANCH" \
--title "chore: bump version to ${NEW_VERSION}" \
--body "Automated version bump to ${NEW_VERSION}.")
echo "pr_url=$PR_URL" >> "$GITHUB_OUTPUT"
PR_NUMBER=$(echo "$PR_URL" | grep -oE '[0-9]+$')
echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT"

- name: Enable auto-merge and merge PR
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ steps.bump_pr.outputs.pr_number }}
shell: bash
run: |
set -euo pipefail
# Enable auto-merge (requires repo setting "Allow auto-merge")
gh pr merge "$PR_NUMBER" --merge --auto || true
# Wait for PR to be merged (poll up to 2 minutes)
for i in {1..24}; do
STATE=$(gh pr view "$PR_NUMBER" --json state -q '.state')
if [[ "$STATE" == "MERGED" ]]; then
echo "PR merged successfully."
exit 0
fi
echo "Waiting for PR to merge... (state: $STATE)"
sleep 5
done
echo "PR did not merge in time. Attempting direct merge..."
gh pr merge "$PR_NUMBER" --merge

- name: Fetch merged main and create tag
env:
TAG: ${{ steps.tag.outputs.tag }}
shell: bash
run: |
set -euo pipefail
git fetch origin main
git checkout main
git pull origin main

echo "Preparing to create tag $TAG"

if git ls-remote --tags origin | grep -q "refs/tags/$TAG$"; then
echo "Tag $TAG already exists on remote. Skipping tag creation."
exit 0
echo "Tag $TAG already exists on remote. Refusing to release." >&2
exit 1
fi

git tag -a "$TAG" -m "Version ${TAG#v}"
git push origin "$TAG"

- name: Clean up release branch
if: always()
env:
GH_TOKEN: ${{ github.token }}
BRANCH: ${{ steps.bump_branch.outputs.name }}
shell: bash
run: |
set -euo pipefail
git push origin --delete "$BRANCH" || true
Comment on lines +180 to +188
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): The release branch is deleted unconditionally, even if the bump PR fails to merge.

Because this step uses if: always(), it deletes the remote release branch even when gh pr merge fails (e.g., due to reviews, checks, or branch protection). That leaves an open PR whose source branch has been deleted, making recovery manual and error-prone.

Please gate this cleanup on the PR actually being merged (or otherwise in a terminal state), e.g. by checking gh pr view or wiring this step to depend on the merge step’s outcome/output before deleting the branch.


- name: Create GitHub release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.tag.outputs.tag }}
name: ${{ steps.tag.outputs.tag }}
generate_release_notes: true

sync_beta:
name: Merge main back into beta via PR
needs:
- bump
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout main
uses: actions/checkout@v6
with:
ref: main
fetch-depth: 0

- name: Create PR to merge main into beta
id: sync_pr
env:
GH_TOKEN: ${{ github.token }}
NEW_VERSION: ${{ needs.bump.outputs.new_version }}
shell: bash
run: |
set -euo pipefail
# Check if beta is behind main
git fetch origin beta
if git merge-base --is-ancestor origin/main origin/beta; then
echo "beta is already up to date with main. Skipping PR."
echo "skipped=true" >> "$GITHUB_OUTPUT"
exit 0
fi

PR_URL=$(gh pr create \
--base beta \
--head main \
--title "chore: sync main (v${NEW_VERSION}) into beta" \
--body "Automated sync of version bump from main into beta.")
echo "pr_url=$PR_URL" >> "$GITHUB_OUTPUT"
PR_NUMBER=$(echo "$PR_URL" | grep -oE '[0-9]+$')
echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT"
echo "skipped=false" >> "$GITHUB_OUTPUT"

- name: Enable auto-merge and merge sync PR
if: steps.sync_pr.outputs.skipped != 'true'
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ steps.sync_pr.outputs.pr_number }}
shell: bash
run: |
set -euo pipefail
# Enable auto-merge (requires repo setting "Allow auto-merge")
gh pr merge "$PR_NUMBER" --merge --auto || true
# Wait for PR to be merged (poll up to 2 minutes)
for i in {1..24}; do
STATE=$(gh pr view "$PR_NUMBER" --json state -q '.state')
if [[ "$STATE" == "MERGED" ]]; then
echo "Sync PR merged successfully."
exit 0
fi
echo "Waiting for sync PR to merge... (state: $STATE)"
sleep 5
done
echo "Sync PR did not merge in time. Attempting direct merge..."
gh pr merge "$PR_NUMBER" --merge

publish_docker:
name: Publish Docker image
needs:
Expand Down
111 changes: 111 additions & 0 deletions docs/guides/RELEASING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# Releasing (Maintainers)

This repo uses a two-branch flow to keep `main` stable for users:

- `beta`: integration branch where feature PRs land
- `main`: stable branch that should match the latest release tag

## Release checklist

### 1) Promote `beta` to `main` via PR

- Create a PR with:
- base: `main`
- compare: `beta`
- Ensure required CI checks are green.
- Merge the PR.

Release note quality depends on how you merge:

- Squash-merging feature PRs into `beta` is OK.
- Avoid squash-merging the `beta -> main` promotion PR. Prefer a merge commit (or rebase merge) so GitHub can produce better auto-generated release notes.

### 2) Run the Release workflow (manual)

- Go to **GitHub → Actions → Release**
- Click **Run workflow**
- Select:
- `patch`, `minor`, or `major`
- Run it on branch: `main`

What the workflow does:

1. Creates a temporary `release/vX.Y.Z` branch with the version bump commit
2. Opens a PR from that branch into `main`
3. Auto-merges the PR (or waits for required checks, then merges)
4. Creates an annotated tag `vX.Y.Z` on the merged commit
5. Creates a GitHub Release for the tag
6. Publishes artifacts (Docker / PyPI / MCPB)
7. Opens a PR to merge `main` back into `beta` (so `beta` gets the bump)
8. Auto-merges the sync PR
9. Cleans up the temporary release branch

### 3) Verify release outputs

- Confirm a new tag exists: `vX.Y.Z`
- Confirm a GitHub Release exists for the tag
- Confirm artifacts:
- Docker image published with version `X.Y.Z`
- PyPI package published (if configured)
- `unity-mcp-X.Y.Z.mcpb` attached to the GitHub Release

## Required repo settings

### Branch protection (Rulesets)

The release workflow uses PRs instead of direct pushes, so it works with strict branch protection. No bypass actors are required.

Recommended ruleset for `main`:

- Require PR before merging
- Allowed merge methods: `merge`, `rebase` (no squash for promotion PRs)
- Required approvals: `0` (so automated PRs can merge without human review)
- Optionally require status checks

Recommended ruleset for `beta`:

- Require PR before merging
- Allowed merge methods: `squash` (for feature PRs)
- Required approvals: `0` (so the sync PR can auto-merge)

### Enable auto-merge (required)

The workflow uses `gh pr merge --auto` to automatically merge PRs once checks pass.

To enable:

1. Go to **Settings → General**
2. Scroll to **Pull Requests**
3. Check **Allow auto-merge**

Without this setting, the workflow will fall back to direct merge attempts, which may fail if branch protection requires checks.

## Failure modes and recovery

### Tag already exists

The workflow fails if the computed tag already exists. Pick a different bump type or investigate why a tag already exists for that version.

### Bump PR fails to merge

If the version bump PR cannot be merged (e.g., required checks fail):

- The workflow will fail before creating a tag.
- Fix the issue, then either:
- Manually merge the PR and create the tag/release, or
- Close the PR, delete the `release/vX.Y.Z` branch, and re-run the workflow.

### Sync PR (`main -> beta`) fails

If the sync PR has merge conflicts:

- The workflow will fail after the release is published (artifacts are already out).
- Manually resolve conflicts in the sync PR and merge it.

### Leftover release branch

If the workflow fails mid-run, a `release/vX.Y.Z` branch may remain. Delete it manually before re-running:

```bash
git push origin --delete release/vX.Y.Z
```