Skip to main content

ViewCompose Publishing

This document defines the local release contract for ViewCompose Maven artifacts and the Android Studio plugin. Remote Maven Central and JetBrains Marketplace uploads are deliberately separate from local preparation so a release can be inspected before any irreversible publication.

Maven identity and version model​

The public Maven namespace is:

com.viewcompose

Every published module owns its version and immutable API source revision in gradle/viewcompose-publishing.properties. Equal version values do not form one atomic release train: changing one entry releases only that artifact and the artifacts whose dependency metadata must point to the new version.

The matching module.<artifact>.sourceRevision is a full 40-character commit SHA. Freeze the module's source in one commit, then update the version and source revision together in a second, metadata-only release commit. This two-step rule avoids self-referential hashes and guarantees that generated Dokka line links resolve to immutable source matching the released module.

Ownership of viewcompose.com and the com.viewcompose namespace is verified in Central Portal. Maven Central releases are immutable, so the namespace and coordinates must be reviewed before the first upload and must not be treated as provisional afterward.

Maven release tags​

Every Maven Central publication must have an immutable Git tag. Because modules evolve independently, ViewCompose does not use a repository-wide v<version> tag for Maven releases. Tag each published artifact using:

maven/<artifact-id>/<version>

For example:

maven/viewcompose-runtime/0.1.0-alpha02
maven/viewcompose-navigation-core/0.2.0
maven/viewcompose-navigation-android/0.2.0

All tags in one Central deployment may point to the same metadata-only release commit. The tag target must be that release commit—not the frozen source commit—because it is the exact repository state that contains the published version and sourceRevision. The signed annotation must record the artifact, version, and frozen source revision so both commits remain auditable.

The annotation contains exactly one sourceRevision=<full-lowercase-40-character-SHA> token. The token may appear in the descriptive sentence shown below or on its own line; the planner accepts both layouts but rejects a missing, malformed, uppercase, or duplicate token. This grammar keeps already-published inline annotations valid without weakening provenance checks for future tags.

Create and push the signed annotated tag only after Central Portal reports the deployment as Published. Do this before starting another release or changing publication metadata:

git tag -s "maven/viewcompose-runtime/0.1.0-alpha02" \
<release-metadata-commit> \
-m "Maven Central: viewcompose-runtime 0.1.0-alpha02; sourceRevision=<frozen-source-commit>"
git push origin "refs/tags/maven/viewcompose-runtime/0.1.0-alpha02"
git ls-remote --exit-code origin \
"refs/tags/maven/viewcompose-runtime/0.1.0-alpha02"

A release is not operationally complete until every artifact tag exists on the remote and resolves to the intended release commit. Never move, delete, or reuse a published tag. Never tag a dirty worktree, a later documentation commit, or a commit whose checked-in metadata differs from the published artifact. If Central publication fails, do not create the final release tag.

First Maven Central release record​

The first Maven Central release published all registered artifacts as 0.1.0-alpha01 from commit dc07ff6189eeab89644e3f9f792e1d7316240812 (build: prepare Maven Central publishing). No Maven-specific tag was created at publication time. On 2026-08-04, after the release checkout was reconstructed from the merged publishing branch and local release chronology, signed annotated tags were created retrospectively for every registered artifact using maven/<artifact-id>/0.1.0-alpha01. Every tag points to that commit and records sourceRevision=dc07ff6189eeab89644e3f9f792e1d7316240812 together with provenance=retrospective.

The unrelated navigation-demo-20260727, navigation-demo-20260727-r2, navigation-demo-20260727-r3, and v0.1.0 repository tags were removed during that remediation; none represented a Maven Central release. A retrospective release tag is allowed only when independent artifact provenance identifies one exact release commit, and its annotation must state that it was reconstructed. Never silently present a retrospective tag as one created during the original publication.

Registered first releases​

An artifact must be registered before its first Central publication, but its final release tag is created only after Central reports Published. Record that temporary state in release.unpublishedModules inside gradle/viewcompose-publishing.properties. Only artifacts in that explicit set may lack a Maven release tag. For them, the planner scans Changesets from repository inception, requires a direct release declaration, keeps the already-registered initial version, and pins the source revision to the clean planning commit without duplicating documentation history.

After the first signed tag is pushed, append the exact published artifact/version/source-revision triple to gradle/viewcompose-documentation-releases.properties and remove the artifact from release.unpublishedModules in the next repository change. Planning fails if an unmarked artifact has no tag, if a marked artifact already has a tag, if the current triple has no immutable documentation record, or if checked-in version metadata has advanced beyond the latest tag. These failures distinguish a genuine first release from missing fetched tags and stale release state.

2026-08-20 coordinated-release provenance correction​

The twelve artifacts first published in the 2026-08-20 coordinated release were built from frozen source commit 143b09acf3bfcda81add008b4dcf09d06a09e2dc. Their signed tags correctly target release commit b8315d326342797b0dee5e2a343ec84d2beaa764, but their annotations inherited registration-time source revisions instead of the frozen source revision. Every affected module changed after its recorded registration revision; three AndroidX-renamed modules did not yet exist under their published artifact names at the recorded revision.

Published tags are immutable and are not moved or replaced. The publishing metadata and the not-yet-merged immutable documentation records use 143b09acf3bfcda81add008b4dcf09d06a09e2dc as the canonical source provenance for those first releases. Release preparation now refreshes a first release's source revision to the clean planning commit while preserving its registered initial version, preventing the registration baseline from being mistaken for released source again. The original signed annotations remain historical evidence of this corrected workflow defect rather than canonical API-source provenance.

Per-pull-request release intent​

ViewCompose records release intent as one immutable Changeset per pull request rather than a shared mutable list of changed modules. A production change to a published artifact is incomplete until a new release/changes/<unique>.json file classifies it. The machine-readable schema is release/changes.schema.json.

{
"schemaVersion": 1,
"summary": "Correct saved-state restoration after process recreation.",
"changes": [
{ "artifact": "viewcompose-runtime", "impact": "fix" }
],
"ignored": [
{
"artifact": "viewcompose-ui-foundation",
"reason": "Only a test fixture changed; no published source or metadata changed."
}
]
}

Direct impact is exactly one of breaking, feature, or fix. Contributors never write dependency: the planner derives it when an independently published dependent must be republished against a changed dependency. ignored is a reviewed no-release classification for an automatically detected artifact and requires a concrete reason. Either changes or ignored satisfies artifact-ownership completeness during pull-request verification and release planning, but only changes supplies direct impact, enters dependency propagation, or satisfies the direct declaration required for a first release. A shared entry may classify an ambiguous root build input such as build.gradle.kts as release-neutral; otherwise the Changeset must declare the artifacts affected by that shared input.

Automatic ownership covers each registered artifact's src/main, src/commonMain, src/androidMain, src/jvmMain, and src/release trees, its publication-relevant module build files, and src/test/samples because compiled API samples affect generated documentation. Ordinary unit/instrumentation tests, Demo code, benchmarks, and handwritten documentation do not request a Maven release by default. Root build files and the version catalog require explicit intent because their effect cannot be inferred safely from a path alone.

Changesets are append-only. Do not modify, rename, delete, or reuse one after merge. Squashed, rebased, and fixup commits remain safe because the release unit is the merged pull request, not an individual commit. A conservative declaration for an artifact that automatic ownership did not detect is allowed; omitting a detected artifact is not.

Verify the current branch against its merge base with origin/main:

./gradlew verifyViewComposeReleaseIntent

CI passes the pull request base SHA through VIEWCOMPOSE_RELEASE_BASE_REVISION. An exceptional local comparison can use -PviewComposeReleaseBaseRevision=<commit>. The task is part of qaQuick and also rejects changes to an already-recorded Changeset.

Deterministic independent release planning​

Run the planner only on a clean, fully fetched checkout whose GPG keyring trusts the ViewCompose release public key:

git fetch origin main --tags
./gradlew planViewComposeRelease

For every previously published artifact, the planner selects the highest semantic version tag matching maven/<artifact-id>/<version>, cryptographically verifies the signed annotation, and reads its single strict sourceRevision token for source and API-documentation provenance. The tag target's immutable release commit—not mutable current publishing metadata—is the comparison and Changeset-consumption boundary because it is the exact repository state used for publication. A Changeset or publication input merged after source freeze but included in that release is therefore not replayed as a new release. Explicit first releases use the clean planning revision and the repository-history rule above. The planner then:

  1. loads Changesets and publication-relevant direct paths introduced between the release-tag target and HEAD;
  2. verifies that every direct path has a matching unconsumed changes or ignored classification;
  3. excludes ignored classifications from the release graph and takes the highest direct impact recorded by changes for the artifact;
  4. derives the current project dependency graph from Gradle api, implementation, compileOnly, and runtimeOnly project dependencies;
  5. propagates a dependency release transitively to every published reverse dependent; and
  6. writes deterministic build/release-plan.json and build/release-plan.md files that separate direct changes from dependency propagation.

Historical Changesets may name coordinates listed in release.retiredModules. The planner treats those identifiers as valid immutable history while computing a first-release baseline, but it never creates a baseline, version recommendation, or dependency-propagated release for a retired coordinate. A retired identifier cannot simultaneously remain an active publication.

The plan recommends, but does not silently choose, versions. Stable lines use semantic versioning: fix and dependency increment patch, feature increments minor, and breaking increments major after 1.0 or minor on a 0.x line. A prerelease increments its existing numeric channel, for example 0.1.0-alpha01 to 0.1.0-alpha02, regardless of impact. The release owner reviews and confirms every exact version.

After the source commit is reviewed and frozen, apply only the confirmed plan:

./gradlew prepareViewComposeRelease \
-PviewComposeReleaseVersions=viewcompose-runtime=0.1.0-alpha02,viewcompose-ui-contract=0.1.0-alpha02

The confirmed artifact set must exactly match the plan. Previously published versions must advance; a first release must keep its registered initial version. The task pins every selected module's sourceRevision to the clean planning commit, updates versions only for previously published artifacts, and appends immutable documentation entries only for those advancing versions. A first release receives its immutable entry after its signed tag is pushed, as described above. Review and commit the prepared diff as the metadata-only release commit. Publication selection must match build/release-plan.json; after Central reports Published, create the signed per-artifact tags described above.

Versioned module manuals rewrite source-relative Markdown targets inside docs/ to their public documentation routes, including Docusaurus number-prefix removal such as 0009-development-tooling-isolation.md to development-tooling-isolation/. Relative repository file targets outside docs/, such as compiled sample sources, become GitHub blob links pinned to the same immutable source revision. Cross-page links with fragments use the canonical English site because the same canonical-English historical snapshot is also served under the zh-CN route and must not resolve an English heading fragment against a translated heading. The production site build remains the authoritative broken-link gate for every generated release snapshot.

The backfill Changeset dated 2026-08-04 classifies publication-relevant changes made after the first Central boundary and before this workflow existed. It is a one-time migration record, not a precedent for reconstructing release intent after merge.

Active-plan archival gate​

Implementation completion is not by itself the Maven publication boundary. Every document under docs/project/plans/, except its directory index, contains exactly one machine-readable ## Maven release changesets section:

## Maven release changesets

- `release/changes/example-feature.json`

A plan that has not started publication-relevant implementation uses one - None. entry instead. This prevents a future plan that happens to mention the same artifact from blocking an earlier, unrelated release. Replace None in the same pull request that adds the plan's first production Changeset, and list every later Changeset owned by that plan. One Changeset cannot belong to two active plans.

Before a public Central upload, verifyArchivedViewComposeReleasePlans parses every active plan, loads its declared immutable Changesets, and derives both their direct artifacts and every transitive reverse-dependent release from the current project dependency graph. The task rejects the upload when that derived set intersects -PviewComposePublishModules and reports the active plans that must close. Moving the completed plan to docs/archive/, updating the active and archive indexes, preserving its final evidence, and passing documentation verification remove the blocker.

The gate intentionally does not block planViewComposeRelease, prepareViewComposeRelease, or local Maven publication because those operations are part of release validation. It is a dependency of the root publishSelectedViewComposeToMavenCentral task and every module-specific Central upload task, so bypassing the root convenience task does not bypass plan acceptance. It can be run alone:

./gradlew verifyArchivedViewComposeReleasePlans \
-PviewComposePublishModules=viewcompose-runtime,viewcompose-navigation-core

Dependency exposure contract​

Published dependencies follow an AndroidX-style capability contract: an application declares the entry-point or optional-feature artifacts it intentionally uses, while those artifacts expose the ViewCompose types required by their public API. A minimal Android application therefore needs only one ViewCompose coordinate:

dependencies {
implementation("com.viewcompose:viewcompose-material3-android:<version-with-this-contract>")
}

viewcompose-material3-android exposes the neutral viewcompose-android aggregate plus the Material 3 adapter. The neutral aggregate exposes UI Foundation, Android Engine, Lifecycle, and ViewModel integrations without Material. viewcompose-host-android remains a low-level engine artifact for advanced mounting and custom integrations. Adding a second direct dependency on an already transitive artifact is harmless Gradle deduplication, but it is redundant and should communicate deliberate direct API usage rather than compensate for incorrect publication metadata.

Feature artifacts expose every ViewCompose module required to compile their public surface, including their platform-neutral core artifact:

dependencies {
implementation("com.viewcompose:viewcompose-navigation-android:0.1.0-alpha02")
implementation("com.viewcompose:viewcompose-animation:0.1.0-alpha05")
implementation("com.viewcompose:viewcompose-gesture:0.1.0-alpha05")
implementation("com.viewcompose:viewcompose-graphics:0.1.0-alpha05")
}

Core artifacts are also independently consumable from Kotlin/JVM modules:

dependencies {
implementation("com.viewcompose:viewcompose-navigation-core:0.1.0-alpha03")
implementation("com.viewcompose:viewcompose-animation-core:0.1.0-alpha05")
implementation("com.viewcompose:viewcompose-gesture-core:0.1.0-alpha05")
implementation("com.viewcompose:viewcompose-graphics-core:0.1.0-alpha02")
}

Classify every direct dependency using these rules:

  1. Use api when a dependency type appears in a public or protected signature, receiver, generic bound, supertype, type alias, or compiled public sample, or when the artifact intentionally acts as the supported entry point for that capability. The only exception is a documented caller-owned platform integration that consumers must already declare to author that platform entry point; its module manual and consumer smoke test must name the direct dependency.
  2. Use implementation only when consumers can compile and use the supported public surface without resolving that dependency on their compile classpath.
  3. Treat ViewCompose and third-party dependencies identically. An import in production source is neither sufficient nor necessary evidence for api; the published contract is the deciding factor.
  4. Do not ask users to declare internal coordinates merely to repair a missing compile edge. Fix the owning artifact's metadata and add a consumer regression instead.
  5. A new published module must define its intended entry-point role and exact dependency exposure before its first release. It must not silently copy the dependency shape of a neighboring module.

gradle/viewcompose-dependency-contracts.properties is the machine-readable allowlist for every registered artifact's direct ViewCompose dependencies. verifyViewComposeDependencyContracts compares it with Gradle declarations, and local repository inspection verifies that api dependencies become Maven compile scope while implementation dependencies become runtime scope. Published-consumption smoke projects then compile the minimal host, optional-feature, and pure-JVM core paths against the generated repository. These checks are part of the publishing configuration and repository verification workflows; changing a dependency edge requires updating the contract, the owning module manual, and release intent together.

Repository Maven samples may adopt an unpublished coordinate only when the same gate first publishes the current checkout to build/maven-repository and then compiles the sample through the generated POM. Public release notes must still distinguish this source-verified state from Maven Central availability. After Central publication succeeds, verify the installation path again from a clean checkout without build/maven-repository.

Gradle Module Metadata preserves api/implementation variant semantics. Maven POMs are also generated for other build tools. Every artifact publishes a sources JAR for IDE source navigation and a javadoc JAR for repository requirements.

ViewCompose does not currently publish a BOM. Modules release independently, and the release planner may propagate dependency-only releases, so a BOM would promise compatibility that has not yet been established for independently versioned combinations. Continue to use explicit versions. Evaluate a BOM only after compatibility tests cover supported mixed-version sets and release automation can update the platform atomically; do not introduce a manually maintained version catalog as a substitute.

The Android host deliberately does not act as an AndroidX or Material version catalog. Applications continue to declare Activity/Fragment and Material dependencies they directly use for their class hierarchy, theme, or optional native interop. This caller-owned exception does not permit hiding a ViewCompose foundation module required by the advertised host DSL.

Local Maven workflow​

Fast metadata validation, without compiling artifacts:

./gradlew verifyViewComposePublishingConfiguration

Publish all registered modules to build/maven-repository:

./gradlew cleanViewComposeLocalRepository publishViewComposeToLocalRepository

Publish only the artifacts that are independently evolving in the current release:

./gradlew publishSelectedViewComposeToLocalRepository \
-PviewComposePublishModules=viewcompose-navigation-core,viewcompose-navigation-android

Selective publication never deletes the repository, so it can resolve already staged independent versions. The all-module task is intended for snapshot QA; public stable releases must use an explicit module selection so unchanged immutable versions are never uploaded again.

Publish and validate only that independent release set:

./gradlew verifySelectedViewComposeLocalRepository \
-PviewComposePublishModules=viewcompose-navigation-core,viewcompose-navigation-android

Inspect an already generated repository without publishing again:

./gradlew inspectViewComposeLocalRepository

Publish and validate primary artifacts, sources, docs, POM metadata, SHA-256/SHA-512 checksums, stable-version signatures, and feature-to-core dependencies:

./gradlew verifyViewComposeLocalRepository

Build four isolated consumers that know nothing about project modules—a neutral Android host, a named Material Android host, an Android feature consumer, and a pure JVM core consumer:

./gradlew verifyViewComposePublishedConsumption

qaQuick publishes the complete current artifact set to the generated local repository so stable signatures and Maven metadata are exercised before merge. qaPreview performs the same local publication first because the Counter preview sample deliberately consumes the public viewcompose-material3-android coordinate instead of a project dependency. Repository inspection and the isolated published-consumer builds remain explicit deeper checks; Maven Central upload tasks are never part of either QA gate.

Version overrides and signing​

The checked-in versions are the source of truth. A CI dry run may override one module without editing the file:

./gradlew publishViewComposeToLocalRepository \
-PviewComposeVersion.viewcompose-navigation-android=0.2.0-SNAPSHOT

The group can be overridden for namespace validation with -PviewComposeGroup=.... An exceptional documentation dry run may override the pinned source with -PviewComposeSourceRevision.<artifact>=<full-commit-sha>; public release metadata must remain checked in and must not rely on this override.

Local stable releases use the machine GPG keyring and OS pinentry window. The release key's public half must be distributed to a Central-supported keyserver; no private key path or passphrase is stored in the project.

CI releases use in-memory PGP signing:

VIEWCOMPOSE_SIGNING_KEY
VIEWCOMPOSE_SIGNING_PASSWORD

Pull-request CI does not have a trusted release key. Each qaQuick and qaPreview job generates a short-lived, unprotected test key inside its disposable runner solely to exercise local stable artifact signing. That key and its artifacts are never uploaded or trusted for a public release. Maven Central workflows must use the in-memory release credentials above.

Stable versions always require signatures; -SNAPSHOT versions may remain unsigned for local QA. Secrets must remain outside the repository.

The Central Portal uploader reads its generated user token from standard private Gradle properties. Put these only in the user-level ~/.gradle/gradle.properties, or inject them through CI secret environment variables:

mavenCentralUsername=<generated token username>
mavenCentralPassword=<generated token password>

For CI, use ORG_GRADLE_PROJECT_mavenCentralUsername and ORG_GRADLE_PROJECT_mavenCentralPassword. Never add either value to this repository.

After changing the selected module versions to stable values and completing the local release checks, create a manual Central Portal deployment with:

./gradlew publishSelectedViewComposeToMavenCentral \
-PviewComposePublishModules=viewcompose-runtime,viewcompose-navigation-core

The task deliberately has no all-module default and rejects -SNAPSHOT versions. It clears one root-owned staging repository, publishes every selected module into that repository, creates build/central-release/viewcompose-central-bundle.zip, and uploads the bundle through the official Portal Publisher API with publishingType=USER_MANAGED. It prints the returned deployment ID, polls until the deployment reaches VALIDATED or FAILED, and writes the ID, state, bundle name, and SHA-256 to build/central-release/viewcompose-central-deployment.json. A failed validation or timeout fails the Gradle build; when an ID was already assigned, the record remains available for Portal diagnosis.

Reaching VALIDATED does not publish immutable artifacts. Review the recorded deployment and its staging consumption before clicking Publish in the Portal. If a run fails after printing an ID, inspect or drop that deployment before retrying; a retry creates another deployment. Coordinated production releases must use the root task above. Module-specific Vanniktech Central tasks remain guarded low-level diagnostics and can create separate deployments, so they are not the coordinated release path.

Android Studio plugin​

The Marketplace plugin version is independently managed by plugin.viewcompose-studio.version in the shared publication properties file. It can be overridden with -PviewComposeStudioPluginVersion=....

Prepare and verify an installable ZIP without uploading:

cd tools/viewcompose-studio-plugin
./gradlew prepareMarketplaceRelease

The first release targets Android Studio build family 261 only and declares com.intellij.modules.androidstudio, preventing Marketplace from advertising the plugin for IntelliJ IDEA products. Both the lower compatibility boundary and untilBuild = 261.* are explicit, so an untested future Android Studio platform is not advertised as compatible by accident. prepareMarketplaceRelease verifies the lower-bound local Quail 2 Patch 1 installation, the current Quail 3 release, and the latest Quail 4 Canary within that advertised window. The first matrix run downloads the latter two IDE distributions; later runs reuse Gradle's IDE cache.

The artifact is written to build/distributions/. Marketplace publishing and signing read only environment variables:

JETBRAINS_MARKETPLACE_TOKEN
JETBRAINS_CERTIFICATE_CHAIN
JETBRAINS_PRIVATE_KEY
JETBRAINS_PRIVATE_KEY_PASSWORD

The standard JetBrains environment names CERTIFICATE_CHAIN, PRIVATE_KEY, PRIVATE_KEY_PASSWORD, and PUBLISH_TOKEN are supported as aliases. Local releases may instead keep chain.crt and private.pem under the default private directory ~/.config/viewcompose/marketplace-signing/. Custom locations can be configured by putting only their absolute paths in user-level ~/.gradle/gradle.properties:

viewComposeMarketplaceCertificateChainFile=/absolute/private/path/chain.crt
viewComposeMarketplacePrivateKeyFile=/absolute/private/path/private.pem
viewComposeMarketplacePrivateKeyPassword=<private key password, only when encrypted>

Build, sign, and verify the author signature before a manual upload:

cd tools/viewcompose-studio-plugin
./gradlew prepareSignedMarketplaceRelease

The Marketplace listing is approved. After the release owner reviews the prepared ZIP, signature, compatibility report, and change notes, follow-up releases can use:

./gradlew publishPlugin

Use -PviewComposeMarketplaceChannels=default,eap to select channels; the default is default.

First public release checklist​

  1. Confirm the com.viewcompose Central namespace remains verified.
  2. Freeze the selected module source in a reviewed commit.
  3. Update each selected version and its sourceRevision to the frozen commit in a metadata-only release commit.
  4. Run qaQuick, verifyCompleteViewComposeApiDocs, verifyViewComposePublishedConsumption, and the relevant release tests.
  5. Require PGP signing and inspect every generated POM, sources JAR, javadoc JAR, and checksum.
  6. Archive every active execution plan linked to the selected release Changesets and run verifyArchivedViewComposeReleasePlans with the exact publication selection.
  7. Upload to a Central staging deployment and verify consumption from that staging repository.
  8. After Central reports Published, create, push, and remotely verify one signed maven/<artifact-id>/<version> tag for every published artifact.
  9. Run prepareMarketplaceRelease, install the ZIP into the target Android Studio build, and do a final preview smoke test.
  10. For the approved plugin listing, review the signed ZIP and compatibility report, then publish the follow-up release with the Marketplace token; a new listing still requires manual review.