Technical Design 0001: Changelog System Replacement
- Issue: #11079
- RFC: Changelog System Replacement
- Status: Proposed
Summary
This design replaces ckChangeLog with a feature-owned changelog reader backed by generated JSON raw resources.
The implementation consists of:
- A generated changelog index resource for each app target.
- Generated per-release changelog JSON resources.
- JSON schema validation before packaging.
- Migration from existing
changelog_master.xmlfiles. - K-9 Mail changelog resource migration from
src/mainto app-target source sets. - A changelog feature data source that reads the index and release resources.
- Preservation of the existing Changelog screen and Recent Changes behavior.
- Removal of
ckChangeLogafter all runtime references are gone.
Current State
The app currently uses ckChangeLog to read XML changelog resources and expose releases to the Changelog and Recent Changes flows.
Relevant areas:
scripts/ci/render-notes.pyscripts/ci/templates/changelog_master.xmlapp-k9mail/src/main/res/raw/changelog_master.xmlapp-thunderbird/src/*/res/raw/changelog_master.xmlfeature/changelog/internalfeature/changelog/internal/src/main/kotlin/net/thunderbird/feature/changelog/internal/ChangeLogManager.ktfeature/changelog/internal/src/main/kotlin/net/thunderbird/feature/changelog/internal/ChangelogViewModel.ktfeature/changelog/internal/src/main/kotlin/net/thunderbird/feature/changelog/internal/RecentChangesViewModel.ktfeature/changelog/internal/build.gradle.ktslegacy/ui/legacy/build.gradle.ktsgradle/libs.versions.tomldocs/release/RELEASE.md
The existing implementation supports:
- Full in-app changelog display.
- Recent Changes display after upgrade.
- Seen-state tracking.
- XML changelog assets.
- Generation of changelog XML from release-note tooling.
The replacement must preserve the user-facing behavior while changing the data format, release tooling, and runtime reader.
Proposed Design
At a high level, the packaged changelog resources move from one XML file per relevant source set to one generated index plus one generated JSON file per release:
Before:
app-k9mail/
`-- src/
`-- main/
`-- res/
`-- raw/
`-- changelog_master.xml
app-thunderbird/
`-- src/
|-- daily/
| `-- res/
| `-- raw/
| `-- changelog_master.xml
`-- debug/
`-- res/
`-- raw/
`-- changelog_master.xml
After:
app-k9mail/
`-- src/
|-- release/
| `-- res/
| `-- raw/
| |-- changelog_index.json
| |-- changelog_release_8_2.json
| `-- changelog_release_8_1.json
`-- debug/
`-- res/
`-- raw/
|-- changelog_index.json
`-- changelog_release_dummy.json
app-thunderbird/
`-- src/
|-- release/
| `-- res/
| `-- raw/
| |-- changelog_index.json
| |-- changelog_release_21_0.json
| `-- changelog_release_20_0.json
|-- beta/
| `-- res/
| `-- raw/
| |-- changelog_index.json
| `-- changelog_release_21_0_b1.json
|-- daily/
| `-- res/
| `-- raw/
| |-- changelog_index.json
| `-- changelog_release_21_0_a1_2026_05_29.json
`-- debug/
`-- res/
`-- raw/
|-- changelog_index.json
`-- changelog_release_dummy.json
The daily example includes the date because the daily version name suffix, a1, is constant. The generator should append
the date to the sanitized daily release resource name so daily builds do not collide.
Debug source sets package dummy changelog JSON files for testing the UI and do not mirror real release history.
The dummy release file should contain representative changelog content only; it is not generated from release notes and
must not be used for release, beta, or daily builds.
Runtime Asset
Each app target packages one generated changelog index raw resource:
R.raw.changelog_index
The index points to generated per-release raw resources. Android cannot scan arbitrary files in res/raw, so the index
is the app’s discovery mechanism.
The layout matches thunderbird-notes, where release notes are maintained as one file per release. Generation can
transform each release source file into one packaged release resource and update the index separately.
Expected generated index files:
app-k9mail/src/release/res/raw/changelog_index.json
app-thunderbird/src/release/res/raw/changelog_index.json
app-thunderbird/src/beta/res/raw/changelog_index.json
app-thunderbird/src/daily/res/raw/changelog_index.json
Expected debug dummy index files:
app-k9mail/src/debug/res/raw/changelog_index.json
app-thunderbird/src/debug/res/raw/changelog_index.json
Expected debug dummy release files:
app-k9mail/src/debug/res/raw/changelog_release_dummy.json
app-thunderbird/src/debug/res/raw/changelog_release_dummy.json
Expected per-release file naming:
changelog_release_<version>.json
changelog_release_<version>_<date>.json
Example:
app-thunderbird/src/release/res/raw/changelog_release_17_0.json
app-thunderbird/src/beta/res/raw/changelog_release_10_0_b1.json
app-thunderbird/src/beta/res/raw/changelog_release_10_0_b2.json
app-thunderbird/src/daily/res/raw/changelog_release_21_0_a1_2026_05_29.json
The release filename is derived from the release version. Daily release filenames also append the date because daily
builds keep the same a1 version suffix throughout a release cycle. The generator must sanitize the version and date for
Android raw resource naming by lowercasing them, separating trailing prerelease labels from the numeric version with _,
and replacing characters outside [a-z0-9_] with _. For example, 10.0b1 becomes 10_0_b1 and 2026-05-29 becomes
2026_05_29.
K-9 Mail currently stores changelog_master.xml in app-k9mail/src/main/res/raw. The JSON replacement should change that
structure to be consistent with Thunderbird for Android during migration and switch to:
app-k9mail/src/release/res/raw/changelog_index.json
app-k9mail/src/debug/res/raw/changelog_index.json
The index must list every packaged release resource. The app must not try to discover changelog files by scanning
res/raw.
JSON Schema
Add schemas at:
scripts/ci/schemas/changelog-index.schema.json
scripts/ci/schemas/changelog-release.schema.json
Use JSON Schema draft 2020-12.
Index shape:
{
"schemaVersion": 1,
"releases": [
{
"version": "21.0",
"date": "2026-05-29",
"url": "https://github.com/thunderbird/thunderbird-android/releases/tag/THUNDERBIRD_21_0",
"resourceName": "changelog_release_21_0"
}
]
}
Release file shape:
{
"schemaVersion": 1,
"version": "21.0",
"date": "2026-05-29",
"url": "https://github.com/thunderbird/thunderbird-android/releases/tag/THUNDERBIRD_21_0",
"notes": [
{
"type": "fixed",
"text": "Fixed a crash when opening the message list",
"issues": [12345],
"pullRequests": [10301],
"featureFlags": ["use_compose_for_message_reader"]
}
]
}
Required index fields:
schemaVersion: integer, must be1.releases: array of release index objects.
Required release index fields:
version: non-empty string.date: string matchingYYYY-MM-DD.resourceName: non-empty string matching the generated raw resource name forversion, plusdatefor daily entries.
Optional release index fields:
summary: non-empty string.url: URI string.
Required release file fields:
schemaVersion: integer, must be1.version: non-empty string.date: string matchingYYYY-MM-DD.notes: array of note objects.
Optional release file fields:
summary: non-empty string.url: URI string.
Required note fields:
type: enum, one ofnew,changed, orfixed.text: non-empty string.
Optional note fields:
id: stable non-empty string.issues: array of positive integers.pullRequests: array of positive integers.featureFlags: array of non-empty strings.source: enum, one ofthunderbird-notesorckchangelog-xml.
The schemas must use additionalProperties: false for every object.
The index and release file for the same release must agree on version and date.
Data Semantics
Release ordering:
- Releases sort by descending
date. - Recent Changes matches the full current app version name to the release
versionfor beta and release builds. - Recent Changes matches both the full current app version name and the current date for daily builds.
- Per-release resource names are derived from the release
version, matchingthunderbird-notes. - Daily per-release resource names also include the
dateto avoid collisions while the daily version name suffix remainsa1.
Note ordering:
- Notes render in the order they appear in the release file.
- Generation must preserve source note order from
thunderbird-notes. - For
thunderbird-notesfiles withgroups, generation must preserve source note order within each generated release file after filtering by release group. - XML migration must preserve source XML note order after classification.
Note types:
new: new user-visible functionality or newly supported capability.changed: behavior changes, improvements, removals, translations, and entries that cannot be confidently classified.fixed: bug fixes, crash fixes, regressions fixed, and security fixes.
Text rendering:
- v1 note text is plain text.
- Markdown rendering is out of scope for the first replacement.
Feature flags:
- Feature-gated notes must use explicit
featureFlagsmetadata. - Feature flags must not be inferred from note text.
- Packaged changelog JSON must contain only notes that are available for the generated app target.
URLs:
- Release URLs should point to GitHub releases when reliably constructible.
- XML migration must not invent URLs.
Issue and pull request references:
issuesmaps fromthunderbird-notesissues.pullRequestsmaps fromthunderbird-notespull_requests.- These fields are the canonical link metadata for notes.
- XML migration must not invent issue or pull request references.
App-specific notes:
thunderbird_only: truenotes are emitted only for Thunderbird targets.k9mail_only: truenotes are emitted only for K-9 Mail targets.- Notes without app-specific metadata are emitted for both apps, subject to feature flag filtering.
- If both
thunderbird_onlyandk9mail_onlyare true, generation must fail.
JSON Generation
Extend the existing release-note tooling instead of adding runtime parsing.
Primary entry point:
scripts/ci/render-notes.py
Generation behavior:
- Read release notes from
thunderbird-notes. - Create one release file for each entry in
release.releases. - Use the release entry
versionandrelease_dateas the generated releaseversionanddate. - For grouped beta notes, include only notes whose
groupbelongs to the generated release entry. - Map release-note tags to
new,changed, orfixed. - Prefer long-form note text for the packaged in-app changelog when both long-form and store-facing text are available.
- Preserve note text, release summary, release URL, release date, issues, pull requests, source note order, and explicit feature flag metadata when available.
- Filter app-specific notes for the generated app target using
thunderbird_onlyandk9mail_only. - Filter feature-gated notes for the generated app target.
- Write or replace each per-release changelog file.
- Load the existing
changelog_index.jsonwhen present. - Insert or replace release index entries by
version. - Sort index entries descending by
date. - Validate the index and release files against their schemas.
- Write the target
changelog_index.json.
Existing store-facing release-note outputs must continue to work during the transition. Store-facing outputs are not the preferred source for the packaged in-app changelog when long-form content exists.
Feature Flag Handling
Feature-gated changelog notes require explicit metadata in thunderbird-notes.
Relevant feature flag factory locations include:
app-thunderbird/src/release/kotlin/net/thunderbird/android/featureflag/TbFeatureFlagFactory.kt
app-thunderbird/src/beta/kotlin/net/thunderbird/android/featureflag/TbFeatureFlagFactory.kt
app-thunderbird/src/daily/kotlin/net/thunderbird/android/featureflag/TbFeatureFlagFactory.kt
app-thunderbird/src/debug/kotlin/net/thunderbird/android/featureflag/TbFeatureFlagFactory.kt
app-k9mail/src/release/kotlin/app/k9mail/featureflag/K9FeatureFlagFactory.kt
app-k9mail/src/debug/kotlin/app/k9mail/featureflag/K9FeatureFlagFactory.kt
Generation behavior:
- If a note has no explicit feature flags, emit it normally.
- If a note has feature flags, resolve them for the generated app target.
- If all required flags are enabled, emit the note with
featureFlags. - If any required flag is disabled, omit the note from the packaged target JSON.
- If a flag cannot be resolved, fail generation.
Runtime debug overrides do not affect packaged changelog JSON.
XML Migration
Add a migration tool for historical XML changelog data.
scripts/migrate-changelog-xml-to-json.py
The migration tool lives under scripts because it is repository-level migration tooling. It is separate from
scripts/ci/render-notes.py, which remains responsible for normal release-note generation from thunderbird-notes.
The migration tool must:
- Accept an explicit input XML path.
- Accept an explicit output resource directory.
- Parse XML with a structured XML parser.
- Convert releases and notes into schema v1 index and release files.
- Sort output deterministically.
- Validate output against the schemas.
- Report migration counts.
Example:
python scripts/migrate-changelog-xml-to-json.py \
--input app-k9mail/src/main/res/raw/changelog_master.xml \
--output-dir app-k9mail/src/release/res/raw
For K-9 Mail, migration reads the legacy src/main XML file and writes JSON output to the target source set that will
package it.
Migration classification should be deterministic and conservative:
- Prefixes such as
New:,Added:, andAdd:map tonew. - Prefixes such as
Fixed:andFix:map tofixed. - Prefixes such as
Changed:,Updated:,Improved:, andRemoved:map tochanged. - Unmatched notes default to
changed.
Migration must not invent URLs, issues, pull requests, feature flags, breaking-change metadata, or availability metadata.
Android Runtime
The changelog feature owns the replacement runtime code.
Runtime requirements:
- Add internal serializable models for schema v1.
- Add a data source that reads
R.raw.changelog_index. - Inject the app version provider and use the full app version name, including build-type suffixes, for Recent Changes lookup. Daily builds must also match the current date.
- Resolve each
resourceNamefrom the index to the corresponding generated raw resource ID. - Load per-release JSON files on demand for the Changelog screen.
- Load only the release file matching the current app version name for Recent Changes, and also matching the current date for daily builds.
- Replace
ChangeLogManagerinternals without changing public navigation APIs. - Throw an exception in debug builds when the index or a required release file is missing, has an unsupported schema version, or contains invalid JSON.
- Return an empty changelog in release, beta, and daily builds when the index or a required release file is missing, has an unsupported schema version, or contains invalid JSON.
- Use
net.thunderbird.core.logging.Loggerfor non-PII diagnostics when decode fails. - Map explicit JSON note types to the existing
NEW,CHANGED, andFIXEDUI categories. - Keep the existing Compose UI layout. Adding new display fields requires a later design.
Recent Changes
Preserve current Recent Changes behavior:
- Fresh install does not show the Recent Changes snackbar.
- Upgrade shows the snackbar once when Recent Changes is enabled, the release matching the current app version has notes, and that version has not been marked as seen.
- Opening or dismissing Recent Changes records the current app version as seen.
Seen state should be stored in feature-owned namespaced storage. Migration from ckChangeLog seen state is optional
until the RFC open question is resolved.
Dependency Removal
After the JSON reader is active and verified:
- Remove
ckChangeLogfrom changelog feature dependencies. - Remove
ckChangeLogfrom legacy UI dependencies once unused. - Remove
ckChangeLogfrom the version catalog. - Remove obsolete XML runtime references.
- Keep or remove XML generation only according to the final release-tooling rollout decision.
Search terms for cleanup:
de.cketti
ckchangelog
ChangeLog.newInstance
ReleaseItem
changelog_master
Migration and Rollout
Implement the change in reviewable slices:
- Add schema and validation tests.
- Add JSON generation beside the existing XML generation.
- Add XML migration tooling and migrate selected historical assets.
- Replace the changelog feature runtime reader.
- Remove
ckChangeLogdependencies and obsolete runtime references. - Update release documentation to make JSON generation and validation part of the release flow.
JSON generation can run beside XML generation until the app runtime switches to JSON. After the runtime switch is
complete and verified, ckChangeLog can be removed.
Testing and Verification
Schema validation:
python -m jsonschema -i app-k9mail/src/release/res/raw/changelog_index.json scripts/ci/schemas/changelog-index.schema.json
python -m jsonschema -i app-thunderbird/src/release/res/raw/changelog_index.json scripts/ci/schemas/changelog-index.schema.json
python -m jsonschema -i app-thunderbird/src/release/res/raw/changelog_release_21_0.json scripts/ci/schemas/changelog-release.schema.json
Schema tests should verify:
- Valid fixtures pass.
- Unknown fields fail.
- Missing required fields fail.
- Unsupported note types fail.
- Invalid URLs fail where URI format validation applies.
Generator tests should verify:
- Type mapping from
thunderbird-notes. - Invalid type rejection.
- Per-release file insert and replace by
version. - Index insert and replace by
version. - Resource name generation from
version. - Resource name generation from beta versions such as
10.0b1. - Multiple release files generated from one
thunderbird-notesfile with multiplerelease.releasesentries. - Grouped beta notes are emitted only to the matching generated release file.
- Descending release ordering by
date. - Source note order preservation.
issuespreservation.pull_requeststopullRequestsmapping.- Thunderbird-only note inclusion for Thunderbird targets.
- Thunderbird-only note omission for K-9 Mail targets.
- K-9 Mail-only note inclusion for K-9 Mail targets.
- K-9 Mail-only note omission for Thunderbird targets.
- Conflicting app-specific note metadata fails generation.
- Feature-gated note inclusion.
- Feature-gated note omission.
- Unresolved feature flags fail generation.
- Missing release resources referenced by the index fail validation.
- Schema validation before writing.
Migration tests should verify:
- XML releases and notes are parsed.
- Prefix classification works.
- Unmatched notes default to
changed. - Source XML note order is preserved after classification.
- Migrated index and release files validate against the schemas.
Android tests should cover:
- Valid JSON maps to existing release UI models.
- Missing or invalid index JSON returns an empty changelog and logs a non-PII diagnostic.
- Missing or invalid release JSON is handled defensively and logs a non-PII diagnostic.
- Recent Changes is not shown on fresh install.
- Recent Changes is shown once after upgrade when the release matching the current app version has notes.
- Opening or dismissing Recent Changes records the current app version as seen.
Open Technical Questions
None.