diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..bdc19045 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,106 @@ +name: Release + +# Publishes to npm automatically once `Build` has passed on master. +# +# The version and changelog come from release-it + @release-it/conventional-changelog +# (see the "release-it" block in package.json), so they are derived from the +# conventional commit messages since the previous tag. +# +# Requires an `NPM_TOKEN` repository secret holding an npm access token with +# publish rights on @react-native-menu/menu. +on: + workflow_run: + workflows: ["Build"] + types: + - completed + branches: + - master + # Manual escape hatch, e.g. to retry a release that failed partway through. + # Note this path does not check Build, so only run it on a commit you know is green. + workflow_dispatch: + +concurrency: + group: release + cancel-in-progress: false + +jobs: + release: + name: Publish to npm + runs-on: ubuntu-latest + # Never react to the `chore: release` commit release-it pushes itself, + # and never publish off a red Build. + if: >- + github.event_name == 'workflow_dispatch' || + (github.event.workflow_run.conclusion == 'success' && + !startsWith(github.event.workflow_run.head_commit.message, 'chore: release')) + permissions: + # release-it pushes the release commit and tag, and creates the GitHub release. + contents: write + steps: + - uses: actions/checkout@v4 + with: + ref: master + # Full history and tags, so conventional-changelog can see the commits + # since the previous release. + fetch-depth: 0 + + - name: Decide whether to release + id: guard + run: | + set -euo pipefail + + # `Build` validated a specific commit. If master has moved on since then, + # publishing now would ship code CI never checked. + if [ "${{ github.event_name }}" = "workflow_run" ]; then + expected='${{ github.event.workflow_run.head_sha }}' + actual="$(git rev-parse HEAD)" + if [ "$expected" != "$actual" ]; then + echo "master moved past the commit Build validated ($expected -> $actual)." + echo "Skipping; the next green Build will pick this up." + echo "release=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + fi + + last_tag="$(git describe --tags --abbrev=0 2>/dev/null || true)" + if [ -n "$last_tag" ]; then + range="$last_tag..HEAD" + else + range="HEAD" + fi + + # Only feat/fix/perf and breaking changes produce a version bump under the + # angular preset. Without one, release-it has nothing to do. + releasable="$(git log "$range" --format='%s%n%b' \ + | grep -cE '^(feat|fix|perf)(\([^)]*\))?!?: |^BREAKING[ -]CHANGE:' || true)" + + echo "Releasable commits since ${last_tag:-the start of history}: $releasable" + if [ "$releasable" -eq 0 ]; then + echo "release=false" >> "$GITHUB_OUTPUT" + else + echo "release=true" >> "$GITHUB_OUTPUT" + fi + + - uses: actions/setup-node@v4 + if: steps.guard.outputs.release == 'true' + with: + node-version: 22 + registry-url: https://registry.npmjs.org + + - name: Install dependencies + if: steps.guard.outputs.release == 'true' + run: yarn install --immutable + + - name: Configure git identity + if: steps.guard.outputs.release == 'true' + run: | + git config user.name 'github-actions[bot]' + git config user.email 'github-actions[bot]@users.noreply.github.com' + + - name: Release + if: steps.guard.outputs.release == 'true' + run: yarn release --ci + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # setup-node writes an .npmrc that reads NODE_AUTH_TOKEN. + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7e00ca57..09e10bca 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -113,6 +113,27 @@ When you're sending a pull request: - For pull requests that change the API or implementation, discuss with maintainers first by opening an issue. - For version-dependent changes, follow the versioning structure for `MenuViewManager` outlined in the **Versioning in `MenuViewManager`** section. Ensure all version-specific files are included in `reactNativeVersionPatch` and referenced in `build.gradle`. +### Releasing + +Releases are automated. Merging to `master` is all that is needed — no one runs `yarn release` locally. + +`.github/workflows/release.yml` waits for the `Build` workflow to finish on `master` and then, if it passed, runs `release-it` in CI. `@release-it/conventional-changelog` derives the new version and the release notes from the commit messages since the previous tag, so **the version bump is decided entirely by your commit message**: + +- `fix:` or `perf:` → patch +- `feat:` → minor +- `!` after the type, or a `BREAKING CHANGE:` footer → major + +A push containing only `chore:`, `docs:`, `test:`, or `refactor:` commits releases nothing, and the workflow stops before touching npm. Note that nothing currently enforces the commit format, so a `feat` mislabelled as `chore` silently ships nothing — squash-merge titles matter here. + +The workflow publishes to npm, pushes the `chore: release ` commit and the `v` tag, and creates the GitHub release. It authenticates with an `NPM_TOKEN` repository secret; if publishing starts failing with an auth error, that token has most likely expired. + +Two things the workflow deliberately refuses to do: + +- It skips if `master` has moved past the commit `Build` validated, rather than publishing code CI never checked. The next green `Build` picks it up. +- It ignores its own `chore: release` commit, so a release cannot trigger another release. + +`Release` can also be started manually from the Actions tab, which is useful when a release fails partway through. That path skips the `Build` gate, so only use it on a commit you know is green. + ## Code of Conduct ### Our Pledge diff --git a/example/android/gradle.properties b/example/android/gradle.properties index b4687e9b..11f7ca80 100644 --- a/example/android/gradle.properties +++ b/example/android/gradle.properties @@ -11,7 +11,7 @@ # particularly useful for configuring JVM memory settings for build performance. # This does not affect the JVM settings for the Gradle client VM. # The default is `-Xmx512m -XX:MaxMetaspaceSize=256m`. -org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 +org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 # When configured, Gradle will fork up to org.gradle.workers.max JVMs to execute # projects in parallel. To learn more about parallel task execution, see the