diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2735622ee..fd5fd74b3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,5 +1,9 @@ name: Release +concurrency: + group: release-main + cancel-in-progress: false + on: workflow_dispatch: inputs: @@ -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 @@ -78,14 +94,19 @@ 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." @@ -93,26 +114,79 @@ jobs: 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 + - name: Create GitHub release uses: softprops/action-gh-release@v2 with: @@ -120,6 +194,70 @@ jobs: 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: diff --git a/docs/guides/RELEASING.md b/docs/guides/RELEASING.md new file mode 100644 index 000000000..89119922f --- /dev/null +++ b/docs/guides/RELEASING.md @@ -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 +```