diff --git a/.github/release-please-config.json b/.github/release-please-config.json new file mode 100644 index 000000000..4532c5e8b --- /dev/null +++ b/.github/release-please-config.json @@ -0,0 +1,32 @@ +{ + "bootstrap-sha": "228d872f9d460173d794b37f49cf4ac042d9826e", + "packages": { + ".": { + "release-type": "simple", + "changelog-path": "CHANGELOG.md", + "include-v-in-tag": true, + "tag-separator": "", + "draft": false, + "prerelease": false, + "bump-minor-pre-major": false, + "bump-patch-for-minor-pre-major": false, + "extra-files": [ + { + "type": "generic", + "path": "config.properties" + } + ], + "changelog-sections": [ + {"type": "feat", "section": "πŸ—οΈ Features"}, + {"type": "fix", "section": "πŸ› οΈ Fixes"}, + {"type": "deps", "section": "πŸ“¦ Dependencies"}, + {"type": "refactor", "section": "♻️ Refactors", "hidden": true}, + {"type": "chore", "section": "πŸ”§ Chores", "hidden": true}, + {"type": "docs", "section": "πŸ“ Docs", "hidden": true}, + {"type": "test", "section": "πŸ§ͺ Tests", "hidden": true}, + {"type": "build", "section": "πŸ— Build", "hidden": true}, + {"type": "ci", "section": "βš™οΈ CI", "hidden": true} + ] + } + } +} diff --git a/.github/workflows/promote.yml b/.github/workflows/promote.yml index df16866f3..cb9a4f355 100644 --- a/.github/workflows/promote.yml +++ b/.github/workflows/promote.yml @@ -129,8 +129,17 @@ jobs: - name: Push Git Tag on Success if: ${{ inputs.commit_sha != '' }} run: | - git tag ${{ inputs.final_tag }} ${{ inputs.commit_sha }} - git push origin ${{ inputs.final_tag }} + # Create the tag only if it does not already exist on the remote. + # release-please may have already created the production tag (vX.Y.Z) + # when its Release PR was merged, so we check first to avoid failing. + FINAL_TAG="${{ inputs.final_tag }}" + COMMIT_SHA="${{ inputs.commit_sha }}" + if git ls-remote --tags origin "$FINAL_TAG" | grep -q "$FINAL_TAG"; then + echo "Tag $FINAL_TAG already exists on remote (likely created by release-please). Skipping tag push." + else + git tag "$FINAL_TAG" "$COMMIT_SHA" + git push origin "$FINAL_TAG" + fi - name: Update GitHub Release with gh CLI env: diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 000000000..0131562ad --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,60 @@ +name: Release Please + +# Runs on every push to main. release-please: +# 1. Parses Conventional Commit messages since the last release. +# 2. Opens (or updates) a Release PR proposing the next SemVer bump and updating +# CHANGELOG.md, version.txt, and VERSION_NAME_BASE in config.properties. +# 3. When the Release PR is merged, creates a `vX.Y.Z` tag and a draft GitHub Release. +# +# The existing channel-promotion pipeline (Create or Promote Release) remains the +# authoritative deployment path. See docs/versioning.md for the full decision record. + +on: + push: + branches: + - main + +permissions: + contents: write + pull-requests: write + +concurrency: + group: release-please-${{ github.ref }} + cancel-in-progress: false + +jobs: + release-please: + name: Release Please + runs-on: ubuntu-24.04-arm + steps: + - uses: googleapis/release-please-action@v4 + id: release + with: + # A PAT is required so that release-please PRs and the resulting + # release tag/commit trigger follow-on CI workflows (the built-in + # GITHUB_TOKEN does not trigger workflows on its own events). + token: ${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }} + config-file: .github/release-please-config.json + manifest-file: .release-please-manifest.json + + # ── Informational output ──────────────────────────────────────────────── + - name: Print release-please outputs + if: always() + env: + RELEASE_CREATED: ${{ steps.release.outputs.release_created }} + TAG_NAME: ${{ steps.release.outputs.tag_name }} + VERSION: ${{ steps.release.outputs.version }} + PR_NUMBER: ${{ steps.release.outputs.pr }} + run: | + echo "### release-please summary" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + if [[ "$RELEASE_CREATED" == "true" ]]; then + echo "βœ… **Release created:** \`$TAG_NAME\` (v$VERSION)" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "Next steps:" >> "$GITHUB_STEP_SUMMARY" + echo "1. Run the **Create or Promote Release** workflow with \`base_version: $VERSION\` and \`channel: internal\` to start the deployment pipeline." >> "$GITHUB_STEP_SUMMARY" + elif [[ -n "$PR_NUMBER" ]]; then + echo "πŸ”„ **Release PR updated:** #$PR_NUMBER" >> "$GITHUB_STEP_SUMMARY" + else + echo "ℹ️ No releasable commits found since last release β€” no PR created." >> "$GITHUB_STEP_SUMMARY" + fi diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 000000000..d2300db9f --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "2.7.14" +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..a373336df --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,10 @@ +# Changelog + +All notable changes to Meshtastic-Android are documented here. + +This file is maintained automatically by +[release-please](https://github.com/googleapis/release-please). +Do not edit it by hand β€” open a conventional-commit PR and the next +Release PR will include your changes automatically. + +See [RELEASE_PROCESS.md](RELEASE_PROCESS.md) for the full release workflow. diff --git a/RELEASE_PROCESS.md b/RELEASE_PROCESS.md index 8a1feb203..02079bd38 100644 --- a/RELEASE_PROCESS.md +++ b/RELEASE_PROCESS.md @@ -2,6 +2,8 @@ This guide summarizes the steps for releasing a new version of Meshtastic-Android. The process is fully automated via GitHub Actions and Fastlane. +For the versioning policy (`versionCode` formula, SemVer bump rules, and the release-please integration), see [docs/versioning.md](docs/versioning.md). + ## Overview The entire release process is managed by a single, manually-triggered GitHub Action: **`Create or Promote Release`**. @@ -20,6 +22,25 @@ The entire release process is managed by a single, manually-triggered GitHub Act ## Release Steps +### 0. Prepare the release (release-please) + +The repository runs a **release-please** pilot that automatically maintains a **Release PR** on +`main`. This PR proposes the next SemVer version bump (determined from +[Conventional Commit](https://www.conventionalcommits.org/) prefixes) and updates: +- `CHANGELOG.md` +- `version.txt` +- `VERSION_NAME_BASE` in `config.properties` + +When the team is ready to cut a new version: + +1. Review the open Release PR (titled `chore(main): release X.Y.Z`) in the repository. +2. Make any changelog edits inside the PR using the `BEGIN_COMMIT_OVERRIDE` / `END_COMMIT_OVERRIDE` mechanism (see [docs/versioning.md](docs/versioning.md)). +3. **Merge the Release PR.** release-please will tag the commit as `vX.Y.Z` and open a draft GitHub Release. +4. Proceed with the internal build (Step 1 below) using the newly bumped version. + +> If you need to force a specific version, push an empty commit with `Release-As: x.y.z` in the +> body before merging the Release PR. + ### 1. Start an Internal Release 1. Navigate to the **Actions** tab in the GitHub repository. @@ -52,6 +73,10 @@ After testing is complete on all pre-release channels, you can create the final 3. Select the `production` channel. 4. The workflow will create a clean version tag (e.g., `v2.4.0`) and create a **published, stable** (non-prerelease) release on GitHub. +> **Note:** If the release-please Release PR was merged before this step, the `v2.4.0` tag +> already exists (created by release-please). The production promotion workflow detects this +> and skips the duplicate tag push gracefully. + ### 4. Post-Release 1. **Verify:** Check the Google Play Console to ensure the build is available on the correct track. diff --git a/config.properties b/config.properties index de820bc85..7a796da72 100644 --- a/config.properties +++ b/config.properties @@ -24,10 +24,11 @@ MIN_SDK=26 TARGET_SDK=37 COMPILE_SDK=37 -# Base version name for local development and fallback -# On CI, this is overridden by the Git tag -# Before a release, update this to the new Git tag version +# Base version name for local development and fallback. +# On CI this is overridden by the Git tag. +# x-release-please-start-version VERSION_NAME_BASE=2.7.14 +# x-release-please-end-version # Minimum firmware versions supported by this app MIN_FW_VERSION=2.5.14 diff --git a/docs/versioning.md b/docs/versioning.md new file mode 100644 index 000000000..995222853 --- /dev/null +++ b/docs/versioning.md @@ -0,0 +1,140 @@ +# Meshtastic-Android Versioning Standards + +This document describes how `versionCode` and `versionName` are computed for all Android and +Desktop builds, and how the project integrates `release-please` for automated changelog and +version-name management. + +--- + +## 1. `versionCode` β€” canonical source of truth + +### Formula + +``` +versionCode = git rev-list --count HEAD + VERSION_CODE_OFFSET +``` + +`VERSION_CODE_OFFSET` is stored in `config.properties` and is a one-time fixed integer large +enough to ensure the resulting `versionCode` is always higher than any code published before +this monotonic scheme was adopted. + +### Why commit-count + offset? + +| Property | Rationale | +|---|---| +| **Monotonically increasing** | Every merged commit increments the count, satisfying Google Play's strict "must never decrease" requirement across all tracks. | +| **Deterministic** | Given a full-history clone and the offset constant, any machine reproduces the same value β€” no "who ran the CI" race. | +| **CI-friendly** | `lint-check` computes it once and passes it via `VERSION_CODE` env-var to downstream jobs that use `fetch-depth: 1`, preserving speed. | +| **Human-readable** | The value visually conveys how many commits exist in the repo, which is useful in support contexts. | + +### Multi-track monotonicity + +Google Play maintains a single `versionCode` pool across all release tracks (internal, alpha, +beta, production). The commit-count formula guarantees global monotonicity because: + +- Internal builds always point to the most recent commit on `main`. +- Subsequent promotions (`internal β†’ closed β†’ open β†’ production`) re-use the **same** build artifact + and therefore the **same** `versionCode`. +- No build produced from a later commit can have a lower code than one produced from an earlier + commit, regardless of track. + +### ABI-split `versionCode` + +The project builds per-ABI APKs for F-Droid/IzzyOnDroid. Each ABI gets a unique code via the +standard Android `splits.abi` mechanism, which uses `versionCodeOverride`: + +| ABI | Multiplier | +|---|---| +| `armeabi-v7a` | base | +| `arm64-v8a` | base + 1 | +| `x86` | base + 2 | +| `x86_64` | base + 3 | + +### Requirements for CI correctness + +`git rev-list --count HEAD` requires a **full-history** clone. Wherever `versionCode` is +calculated (CI `lint-check` job, release workflow `prepare-build-info` job), the checkout +**must** use `fetch-depth: 0`. Shallow clones produce incorrect counts and are explicitly +rejected by `GitVersionValueSource`. + +--- + +## 2. `versionName` β€” canonical source of truth + +`versionName` follows **Semantic Versioning** (`MAJOR.MINOR.PATCH`). + +| Context | Source | +|---|---| +| Local dev / debug builds | `VERSION_NAME_BASE` in `config.properties` | +| CI PR / push builds | `VERSION_NAME_BASE` in `config.properties` | +| Release builds | `VERSION_NAME` env-var injected by Fastlane / CI, derived from the Git tag (`v2.7.14` β†’ `2.7.14`) | + +The displayed version string for product flavors appends the versionCode and flavor name: + +``` +2.7.14 (29314800) fdroid +``` + +--- + +## 3. Updating `VERSION_NAME_BASE` β€” the release-please pilot + +Starting with the 2.7.x series, the project runs `release-please` as a non-shipping assistant +that proposes version bumps and maintains `CHANGELOG.md` automatically. + +### How it works + +1. Every push to `main` triggers `.github/workflows/release-please.yml`. +2. `release-please` parses [Conventional Commit](https://www.conventionalcommits.org/) messages + (`feat:`, `fix:`, `feat!:` / `BREAKING CHANGE`, etc.) and determines the next SemVer bump. +3. It opens (or updates) a **Release PR** that: + - Bumps `version.txt` to the next version. + - Updates the `# x-release-please-start-version` block in `config.properties` + (keeping `VERSION_NAME_BASE` in sync). + - Prepends the new section to `CHANGELOG.md`. +4. When the team is ready to ship, they **merge the Release PR** on GitHub. +5. `release-please` then creates a `v{version}` Git tag and a draft GitHub Release. +6. The team then runs the existing **`Create or Promote Release`** workflow using the new version + as the `base_version` input to build and deploy to the channel pipeline. + +> **Pilot mode:** During the pilot, `skip-github-release` is set to `false` so that a draft +> release is created as an anchor for release-please's version tracking. The existing +> channel-promotion pipeline is still the authoritative deployment path. + +### Conventional Commit β†’ SemVer mapping + +| Commit prefix | SemVer bump | +|---|---| +| `fix:` | patch (`2.7.14 β†’ 2.7.15`) | +| `feat:` | minor (`2.7.x β†’ 2.8.0`) | +| `feat!:` / `BREAKING CHANGE:` | major (`2.x.y β†’ 3.0.0`) | +| `chore:`, `docs:`, `refactor:`, `test:`, `build:` | no bump (not releasable units) | + +### Forcing a specific version + +Add `Release-As: x.y.z` to the **commit body** of any commit on `main` to override the +auto-computed version: + +``` +git commit --allow-empty -m "chore: release 3.0.0" -m "Release-As: 3.0.0" +``` + +### Config files + +| File | Purpose | +|---|---| +| `.github/release-please-config.json` | Strategy, extra-files, bootstrap SHA | +| `.release-please-manifest.json` | Current version per package (updated on each release PR merge) | +| `version.txt` | Primary version file tracked by the `simple` release strategy | +| `CHANGELOG.md` | Auto-generated and maintained by release-please | + +--- + +## 4. Decision record + +| Decision | Rationale | +|---|---| +| **Commit-count + offset for `versionCode`**, not a manually bumped integer | Eliminates human error (forgetting to increment), makes codes deterministic across machines, and automatically handles hotfix branches. | +| **`simple` release-please strategy** instead of `java` / `maven` | This is a Kotlin/Gradle repo; the `simple` strategy's `version.txt` is the least invasive primary file, while `extra-files` keeps `config.properties` in sync. | +| **`skip-github-release: false`** during pilot | Needed so release-please can find its own tags on subsequent runs; the existing channel-promotion pipeline still owns deployment. | +| **Keep existing channel-promotion pipeline** | Google Play's internalβ†’alphaβ†’betaβ†’production promotion model doesn't map onto release-please's single-track model; the existing bespoke pipeline handles this correctly. | diff --git a/version.txt b/version.txt new file mode 100644 index 000000000..9bbf49249 --- /dev/null +++ b/version.txt @@ -0,0 +1 @@ +2.7.14