Skip to content
Open
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
2 changes: 2 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,5 @@ updates:
commit-message:
prefix: chore
include: scope
ignore:
- dependency-name: "gradle"
119 changes: 119 additions & 0 deletions .github/workflows/publish-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
name: Publish - Release

on:
workflow_dispatch:
inputs:
component_path:
description: "Gradle path of the component to publish, e.g. :components:bom"
required: true
default: ":components:bom"

permissions:
contents: write

concurrency:
group: publish-release-${{ inputs.component_path }}
cancel-in-progress: false

jobs:
publish-release:
name: Publish release
runs-on: ubuntu-latest
timeout-minutes: 60

env:
COMPONENT_PATH: ${{ inputs.component_path }}
ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }}
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }}
ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_IN_MEMORY_KEY }}
ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.SIGNING_IN_MEMORY_KEY_ID }}
ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_IN_MEMORY_KEY_PASSWORD }}

steps:
- name: Validate branch
run: |
set -euo pipefail

if [[ "${GITHUB_REF_NAME}" != "main" ]]; then
echo "Releases must be published from main. Current ref: ${GITHUB_REF_NAME}"
exit 1
fi

- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0

- name: Setup Gradle environment
uses: ./.github/actions/setup-gradle

- name: Configure Git author
run: |
git config user.name "TMC Release Bot"
git config user.email "tmc-release-bot@thunderbird.net"

- name: Create release tag
id: release-tag
run: |
set -euo pipefail

tag="$(./gradlew -q "${COMPONENT_PATH}:printReleaseTag")"

if git rev-parse -q --verify "refs/tags/${tag}" >/dev/null; then
tag_commit="$(git rev-list -n 1 "${tag}")"
head_commit="$(git rev-parse HEAD)"

if [[ "${tag_commit}" != "${head_commit}" ]]; then
echo "Release tag ${tag} already exists but does not point at HEAD."
exit 1
fi

echo "Release tag ${tag} already exists on HEAD."
echo "tag=${tag}" >> "${GITHUB_OUTPUT}"
exit 0
fi

./gradlew "${COMPONENT_PATH}:createReleaseTag"

if ! git rev-parse -q --verify "refs/tags/${tag}" >/dev/null; then
echo "Expected release tag ${tag} to be created."
exit 1
fi

echo "tag=${tag}" >> "${GITHUB_OUTPUT}"

- name: Write release notes
run: ./gradlew "${COMPONENT_PATH}:writeReleaseNotes" -PreleaseNotesFile="${RUNNER_TEMP}/release-notes.md"

- name: Publish release
run: ./gradlew "${COMPONENT_PATH}:validateStableVersionForPublishing" "${COMPONENT_PATH}:publishAndReleaseToMavenCentral"

- name: Push release tag
run: |
set -euo pipefail

tag="${{ steps.release-tag.outputs.tag }}"
if git ls-remote --exit-code --tags origin "refs/tags/${tag}" >/dev/null 2>&1; then
echo "Remote release tag ${tag} already exists."
exit 0
fi

git push origin "refs/tags/${tag}"

- name: Create GitHub release
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail

tag="${{ steps.release-tag.outputs.tag }}"
if gh release view "${tag}" >/dev/null 2>&1; then
echo "GitHub release ${tag} already exists."
exit 0
fi

gh release create "${tag}" \
--title "${tag}" \
--target "${GITHUB_SHA}" \
--verify-tag \
--notes-file "${RUNNER_TEMP}/release-notes.md"
72 changes: 72 additions & 0 deletions .github/workflows/publish-snapshot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
name: Publish - Snapshot

on:
workflow_dispatch:

permissions:
contents: write

concurrency:
group: publish-snapshot
cancel-in-progress: false

jobs:
publish-snapshot:
name: Publish snapshot
runs-on: ubuntu-latest
timeout-minutes: 60

env:
ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }}
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }}
ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_IN_MEMORY_KEY }}
ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.SIGNING_IN_MEMORY_KEY_ID }}
ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_IN_MEMORY_KEY_PASSWORD }}

steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: main
fetch-depth: 0

- name: Check snapshot marker
id: snapshot-marker
run: |
set -euo pipefail

marker="snapshot/latest"
head_commit="$(git rev-parse HEAD)"

if git rev-parse -q --verify "refs/tags/${marker}" >/dev/null; then
marker_commit="$(git rev-list -n 1 "${marker}")"

if [[ "${marker_commit}" == "${head_commit}" ]]; then
echo "Snapshot marker ${marker} already points at HEAD. Nothing to publish."
echo "publish=false" >> "${GITHUB_OUTPUT}"
exit 0
fi
fi

echo "publish=true" >> "${GITHUB_OUTPUT}"

- name: Setup Gradle environment
if: steps.snapshot-marker.outputs.publish == 'true'
uses: ./.github/actions/setup-gradle

- name: Publish snapshot
if: steps.snapshot-marker.outputs.publish == 'true'
run: |
set -euo pipefail

./gradlew validateSnapshotVersionForPublishing
./gradlew publishToMavenCentral

- name: Update snapshot marker
if: steps.snapshot-marker.outputs.publish == 'true'
run: |
set -euo pipefail

marker="snapshot/latest"
git tag --force "${marker}" HEAD
git push --force origin "refs/tags/${marker}"
47 changes: 47 additions & 0 deletions .github/workflows/validate-pr-title.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Validate - Pull Request Title

on:
pull_request:
types: [opened, edited, reopened, synchronize]

permissions:
contents: none

concurrency:
group: validate-pr-title-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
validate-pr-title:
name: Validate PR title
runs-on: ubuntu-latest
timeout-minutes: 5

steps:
- name: Validate Conventional Commit title
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: |
set -euo pipefail

pattern='^(feat|fix|docs|style|refactor|perf|test|chore|build|ci|deps|revert)(\([a-zA-Z0-9._ -]+\))?!?: .+$'

if [[ ! "$PR_TITLE" =~ $pattern ]]; then
cat <<'MESSAGE'
Pull request title must use Conventional Commit format because release changelogs are generated from PR merge commits.

Expected format:
<type>(<scope>): <description>

Examples:
feat(account): add OAuth setup
fix(build): restore publishing metadata
chore(deps): update Kotlin

Allowed types:
feat, fix, docs, style, refactor, perf, test, chore, build, ci, deps, revert
MESSAGE
echo
echo "Actual title: $PR_TITLE"
exit 1
fi
10 changes: 10 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ Before you start contributing, please take a moment to familiarize yourself with

- **Mozilla Community Participation Guidelines:** [https://www.mozilla.org/en-US/about/governance/policies/participation/](https://www.mozilla.org/en-US/about/governance/policies/participation/)

### Git Commit Guide

We follow [Conventional Commits](https://www.conventionalcommits.org/). Please read our
[Git Commit Guide](docs/commit-guide.md) for examples, allowed types, scopes, and best practices.

### Release Guide

Maintainers and release owners should read the [Release Guide](docs/release-guide.md) for the
release-preparation workflow, changelog generation, and tagging conventions.

## Bug Reports and Feature Requests

If you encounter a bug or have a feature request, please follow these steps:
Expand Down
61 changes: 61 additions & 0 deletions build-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,61 @@ Supportive/quality and tooling plugins:
- Common tasks: `./gradlew koverHtmlReport`, `./gradlew koverXmlReport`, `./gradlew koverVerify`
- `net.thunderbird.gradle.plugin.publishing` — Configures publishing (Maven coordinates, POM metadata, and common
repositories like `mavenLocal()` and a local build repo under `build/maven-repo`)
- `net.thunderbird.gradle.plugin.versioning` — Reads and updates component versions from the nearest
`version.properties`.
- Version resolution: builds from a commit tagged with `<component>-<version>` use `<version>`;
all other builds use `<version>-SNAPSHOT`.
- Tasks:
- `versionBumpMajor`, `versionBumpMinor`, `versionBumpPatch`: update the nearest `version.properties`.
- `printVersion`: prints the version resolved from the nearest `version.properties`.
- `createReleaseTag`: creates the component release git tag from `version.properties`.
- Format: `<component>-<version>` (for example `bom-1.0.0`)
- Validation: fails for existing tags.
- Release tag command:
```bash
./gradlew :components:bom:createReleaseTag
```

- `net.thunderbird.gradle.plugin.changelog` — Maintains component‑local `CHANGELOG.md` files next to each component’s
nearest `version.properties` (modules that share the same nearest `version.properties` directory are grouped together).
- Task:
- `updateChangelog`: ensures a component‑local `CHANGELOG.md` exists and auto‑populates the
`## Unreleased` section from Conventional Commit titles in git.
- Scope: only commits that touch the component directory (relative to the repository root) are considered.
- History: entries are read from first-parent history on `origin/main`/`main`, so pull request branch commits
are ignored.
- Release boundary: the task uses an exact component tag range based on the latest released changelog version
(for example `bom-1.0.0..main`) and fails when the expected previous release tag does not exist.
- Grouping: entries are grouped by type into sections (`Features`, `Bug Fixes`, `Documentation`, `Styles`,
`Refactoring`, `Tests`, `Chores`, `Reverts`).
- De‑duplication: existing bullets are preserved and duplicates from git subjects are avoided.
- Workflow: run changelog updates during release preparation, not during normal builds. See the
[Release Guide](../docs/release-guide.md).
- `finalizeChangelog`: finalizes the current `## Unreleased` section for a release version and inserts a new
empty `## Unreleased` section above it.
- Version source: nearest `version.properties`
- Optional property: `-PreleaseVersion=<version>` (must match `version.properties` when provided)
- Optional property: `-PreleaseDate=YYYY-MM-DD` (defaults to the current local date)
- Validation: fails when `Unreleased` is missing, empty, or when the target release already exists.
- `writeReleaseNotes`: writes the finalized release section to a markdown file for GitHub Releases.
- Version source: nearest `version.properties`
- Optional property: `-PreleaseNotesFile=<path>` (defaults to `build/release/release-notes.md`)
- Usage:
```kotlin
// In any project that should contribute a component changelog
plugins {
id("net.thunderbird.gradle.plugin.changelog")
}
```

Command:
```bash
# Run for a single component (example: BOM)
./gradlew :components:bom:updateChangelog

# Finalize the changelog for a release
./gradlew :components:bom:finalizeChangelog
```

### Applying a plugin

Expand Down Expand Up @@ -81,6 +136,12 @@ The `net.thunderbird.gradle.plugin.publishing` plugin:
- Sets Maven coordinates from the project and configures POM metadata
- Adds local repositories: `mavenLocal()` and `${rootProject}/build/maven-repo`
- Configures publishing to Maven Central and signs all publications
- Adds release-oriented validation tasks:
- `validateStableVersionForPublishing`: validates a non-`SNAPSHOT` version before release publishing.
- `validateSnapshotVersionForPublishing`: validates a `SNAPSHOT` version before snapshot publishing.
- Adds release tag tasks:
- `printReleaseTag`: prints the component release tag derived from `version.properties`.
- `createReleaseTag`: creates the component release tag locally.

Signing properties can be supplied from a file at `${rootProject}/.signing/signing.properties` with keys:

Expand Down
15 changes: 14 additions & 1 deletion build-plugin/plugin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,18 @@ dependencies {
compileOnly(plugin(libs.plugins.compose.multiplatform))

implementation(plugin(libs.plugins.dependency.check))
implementation(plugin(libs.plugins.dokka))

implementation(plugin(libs.plugins.maven.publish))

implementation(plugin(libs.plugins.kover))
implementation(plugin(libs.plugins.detekt))
implementation(plugin(libs.plugins.spotless))

compileOnly(libs.kotlinx.datetime)
implementation(libs.kotlinx.datetime)

testImplementation(libs.assertk)
testImplementation(libs.kotlin.test)
}

gradlePlugin {
Expand All @@ -40,6 +44,10 @@ gradlePlugin {
id = "net.thunderbird.gradle.plugin.library.kmp.compose"
implementationClass = "net.thunderbird.gradle.plugin.library.kmp.compose.LibraryKmpComposePlugin"
}
register("Bom") {
id = "net.thunderbird.gradle.plugin.bom"
implementationClass = "net.thunderbird.gradle.plugin.bom.BomPlugin"
}

register("DependencyCheck") {
id = "net.thunderbird.gradle.plugin.dependency.check"
Expand Down Expand Up @@ -68,6 +76,11 @@ gradlePlugin {
id = "net.thunderbird.gradle.plugin.versioning"
implementationClass = "net.thunderbird.gradle.plugin.versioning.VersioningPlugin"
}

register("Changelog") {
id = "net.thunderbird.gradle.plugin.changelog"
implementationClass = "net.thunderbird.gradle.plugin.changelog.ChangelogPlugin"
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ object ProjectConfig {
const val group = "net.thunderbird"

object Android {
const val sdkMin = 21
const val sdkMin = 23

// Only needed for application
const val sdkTarget = 35
Expand Down
Loading