- Why
- The fence
- Terminology
- Two senses of “release”
- Versioning: CalVer trains, SemVer products
- Compatibility is recorded, never computed
- Dependencies: planning edges, not a resolved graph
- Repositories: three levels, one class
- The catalog pattern and federation
- Model overview
- Generation
- The GitHub model: assessment
- Sequencing, and a note on the assessment
- Relation to other Nasdanika work
Nasdanika Release Model
Draft. This document and
release.xcoredescribe a design, not a shipped module. Seeassessment.mdfor the prior art survey, the name decision and the sequencing argument, some of which this design answers and some of which it deliberately overrides.
An Ecore model of release engineering across many small interdependent repositories: the products that are released, the versions they ship, where those versions are published, and the coordinated releases that group them. Defined in release.xcore.
It is a planning model and a record model at once. A planned version is an ordinary version with a status of PLANNED and no publications, so the roadmap and the history are the same graph read at different times.
Why
Because independent versioning makes “which versions work together” unanswerable, and something has to answer it. Lerna named the trade twenty years after Eclipse ran into it: fixed mode moves everything to one number and is dishonest about what actually changed, independent mode is honest and leaves consumers without a compatibility story. The artifact that closes the gap is a bill of materials, and a BOM that is maintained by hand across a hundred repositories is wrong within a month. So the BOM is generated, and a generator needs a source of truth.
Because the toil is generation, not tracking. Tracking what is in a release is something a YAML file does adequately. Producing the BOM, the category and aggregator poms, the npm manifests, the extension manifests, the release notes and the per-model status badges is where the hours go, and all of them read the same data. Six consumers is past the threshold where typing the data pays.
Because nothing coordinates one release across four publishing ecosystems. Eclipse and Quarkus coordinate JVM release trains. Lerna, Changesets and release-please coordinate npm packages. JReleaser publishes to many JVM destinations. None of them has a release concept that spans Maven Central, npm, the Visual Studio Marketplace and Open VSX at once, because almost nobody needs one. Generating TypeScript modules from Ecore forces exactly that combination, which is the narrow and defensible claim this model makes.
Because a release that references things is a different artifact from a changelog. A release that knows the JVM modules it publishes, the coverage at that commit, the work it closes and the compatibility evidence behind its version number is not something release-please can represent. That is the tower thesis, and it is the reason this is a model rather than a manifest.
The fence
This is a planning and record model. Execution is a pipeline that reads it. Checkout, build order execution, version bumping, tagging, publishing and recovering from a failure eighteen repositories in are not modelled here and should not be. At a hundred repositories, orchestration is where the real difficulty lives, and letting a metamodel grow into a build orchestrator is the same failure mode as letting the JPMS work grow into a module container.
Terminology
The model uses industry terms where they exist and picks deliberately where the industry is split.
| Concept | Term used here | Why, and what else it is called |
|---|---|---|
| Something released, with versions | Product | Also called a component or a package. Product is free: the product management model defines ProductModel, personas, concerns and capabilities, and no Product class |
| A version of one product | Version | GitHub calls this a release, Maven a version, npm a version |
| A set of product versions shipped together | Release | A release train (Spring, Quarkus), a simultaneous release (Eclipse) |
| A checkpoint inside a release | Milestone | M1, M2, RC1, GA, as Eclipse runs them |
| Where source lives | Repository | Reserved for source, never for publishing |
| The hosting service | Forge (a repository kind) | GitHub, GitLab, Bitbucket, Codeberg. “Forge” is the established word, used by ForgeFed, Debian and Eclipse. “Platform” is vaguer and collides with the Quarkus meaning |
| The owner namespace | Organization (a repository kind) | GitHub says owner or organization, GitLab says group or namespace |
| Where artifacts are published | Registry | Maven says repository, npm and OCI say registry, marketplaces say marketplace. SPDX and CycloneDX settled on registry, and repository is taken |
| A product’s coordinates in a registry | Distribution | Maven distributionManagement, npm publishConfig |
| A version having arrived in a registry | Publication | The audit record |
| Cross-ecosystem coordinates | purl | package URL, the one vocabulary that covers maven, npm, oci and nuget in one string |
The three publishing words are three different things and collapsing them is the mistake worth avoiding: Registry is the place and is shared, Distribution is a product’s standing presence there and is version-less, Publication is one version arriving with a date, a URL and a size.
Two senses of “release”
The word does double duty in every ecosystem, so the model splits it:
Versionis a version of one product:nasdanika-core 1.0.0.Releaseis the coordinated set:2026.9.0, which containsnasdanika-core 1.0.0and whatever else ships with it.
The edge points from version to release, not the other way. That is the Eclipse Simultaneous Release contribution model, where each project contributes an .aggrcon naming the exact version it puts into the train. Adding a product to a train does not mean editing the train, which matters when the train is a file under review and the product is a repository somebody else owns. Release.versions derives the membership, resolving through ReleaseReference so a federated resource can contribute to a train defined elsewhere.
Versioning: CalVer trains, SemVer products
Products carry semantic versions and releases carry calendar versions. That is not a compromise, it is what Eclipse has been doing for years: train 2024-09, per project SemVer. The train date says when, the product version says what changed.
versioningScheme is an attribute rather than an assumption, because an estate moving off CalVer has both schemes at once during the move, and a model that cannot represent the transition is useless exactly when it is needed.
SemVer components are stored as parsed integers beside the authoritative string. version is the identity; major, minor and patch are parsed at load time. The duplication is deliberate, for the same reason the JVM model unpacks access flags into booleans: openCypher has no version comparison and awkward string handling, so “every product still below 1.0.0” has to be a numeric comparison on the query surface this model exists to feed.
Compatibility is recorded, never computed
Do not compute the required version bump. japicmp and Revapi have classified JVM binary and source compatibility for years, Eclipse PDE API Tools has done it for bundles for fifteen, api-extractor produces the TypeScript equivalent, and cargo-semver-checks is the exemplar of doing it well. Wrap them and store the verdict.
CompatibilityAssessment records the analyzer, the baseline version, the required bump and whether binary and source compatibility hold. Version.changeLevel records the bump actually made. The two disagreeing is the finding, and it is a finding no changelog can produce.
Incompatibility.elementUri is the join that makes the record better than a report: a JVM model URI such as jvm:method/org.nasdanika.Foo#bar(Ljava/lang/String;)I resolves to the method, so “this release is major because these three methods were removed” is navigable rather than narrated. That makes API compatibility the fourth instance of the observation-attachment pattern after coverage, telemetry and rule violations, and another argument for building the attachment mechanism once.
Dependencies: planning edges, not a resolved graph
Version.dependencies is coarse and product-version scoped. It exists to sequence work, to topologically sort a build, and to carry effort roll-up for dependents and value roll-up for providers, which is the lightweight stand-in for what the capability model will eventually do properly.
The resolved artifact graph belongs to the Maven model and the resolved module graph to the JVM model, and neither is restated here. An edge can be authored (a planning constraint that exists before any code does) or derived from one of those graphs, and Dependency.derived plus Dependency.source record which, so a rescan never silently overwrites a hand-authored edge. That is the honest answer to the assessment’s ruling against a second dependency representation: the two graphs are at different granularities, and this one can be generated from the other.
Repositories: three levels, one class
GitHub / Nasdanika / core is three levels, and it is one class. Repository extends RepositoryCatalog, so a repository contains repositories, and RepositoryKind distinguishes the levels as instance data rather than as subclasses:
| Level | Kind | Example |
|---|---|---|
| Hosting service | forge | GitHub |
| Owner namespace | organization | Nasdanika |
| Repository | git repository, with github repository as a sub-kind | core |
Kinds are a taxonomy (superType), so a query can ask for every GitHub repository or every git repository and get the right answer without the model knowing which taxonomies exist. This follows the architecture and requirements models, where notations are catalogs and kinds are instance data.
Repositories are catalogued separately from products and referenced by them. That is what lets a repository be loaded, scanned and reported on whether or not a product claims it, which is how an estate report finds the repositories nobody remembered. RepositoryStatistics is a dated contained record rather than attributes, because forge numbers change without anything in the model changing, and an undated number is worse than no number.
The catalog pattern and federation
Products, repositories, registries and releases each follow the AbstractX / X / XReference / XCatalog shape from the product management model. A catalog contains abstract elements, so an entry is a definition, a reference to a definition in another resource, or another catalog. Each catalog exposes a resolvedX derived feature that dereferences references and detects cycles and dangling targets.
ProductCatalog is the resource root and mixes in the other three catalogs. This is the answer to the open question in the outline: the fourth mixin should be RegistryCatalog, not “DistributionCatalog”, because the catalogued shared thing is the registry, while a distribution is a product’s presence in one and belongs to the product.
The consequence of the mixed root is worth knowing before you read the xcore: every nested product catalog can also carry repositories, registries and releases. That is a feature for federation, a resource contributes everything it knows in one file, and a wart in the type system, a ProductCatalog is technically an AbstractRepository. The product management model’s ProductModel makes exactly the same trade, and consistency with the established pattern is worth more here than purity.
Model overview
| Area | Types |
|---|---|
| Root and products | ProductCatalog (root, mixes the three catalogs below, holds poms), AbstractProduct, Product, ProductReference, ProductKind |
| Versions | Version (version string plus parsed major / minor / patch, status, dates, tag, previous, changeLevel, estimate, value), Dependency, DependencyKind |
| Repositories | AbstractRepository, RepositoryCatalog, Repository (extends the catalog, so it nests), RepositoryReference, RepositoryKind, RepositoryStatistics |
| Registries | AbstractRegistry, RegistryCatalog, Registry (purlType, repositoryId), RegistryReference, RegistryKind, Distribution, Publication |
| Releases | AbstractRelease, ReleaseCatalog, Release (derived versions), ReleaseReference, Milestone |
| Compatibility | CompatibilityAssessment, Incompatibility |
| Generation | PomGeneration, PomType |
| Enumerations | VersioningScheme, ChangeLevel, ReleaseStatus, Visibility, PomType |
| Reused, not redefined | NxCore ModelElement, NamedElement, Evaluator |
Two reuse decisions rather than new attributes: release notes live in the inherited documentation and docRef, which is what NxCore’s Documented is for, and notes drafted by an assistant are reviewed and then stored, never generated at read time. And state is a short local enum on purpose, to be replaced by the lifecycle model’s Staged when that dependency is worth taking.
Generation
The model is dependency-light by design (NxCore only), so it can ship before the floors it will eventually reference exist. The CLI commands it is built to feed:
bom generates a bill of materials from a catalog and a release: the release version becomes the BOM version, and every product version in that release becomes a dependencyManagement entry using its Maven Distribution coordinates. This is the Quarkus move, where quarkus-platform-bom-generator produces the platform BOM instead of anyone maintaining it.
poms generates category and aggregator poms following catalog structure, for every catalog that carries a PomGeneration. Containment is the marking: a catalog with no PomGeneration does not produce a pom.
The nesting rule is a rule rather than a flag: when a child catalog has a pom of the same type, the parent includes that pom as a module instead of expanding the child’s products. So tower becomes one module of all-models, and a catalog without its own pom is inlined. Membership can also be a predicate rather than a list, through PomGeneration.filter, which is an NxCore Evaluator: “everything with a Maven distribution” gives the central pom without listing a hundred products twice.
github release creates forge releases from the model rather than by hand: tag, name, documentation as the body, pre-release flag from the version’s pre-release identifiers.
Later, from the same data: npm and extension manifests, release notes, the per-model status badges the publishing strategy needs, and the model published to JSON and TypeScript so it is browsable in the reflective viewer.
The GitHub model: assessment
The outline asks whether a GitHub model should sit under the release model or interchange with it. Interchange, the way Meta interchanges with Ecore, and build it as part of this effort.
Why not a base. The release model has to stay forge-neutral. The repository catalog with kinds is exactly the neutral shape, and it costs nothing to keep: a GitLab group and a Codeberg organization are the same two kinds. Putting a GitHub model underneath would make every consumer of the release model depend on hub4j/github-api and on GitHub’s object shapes, for a model whose whole point is that publishing spans ecosystems. This is the same argument the JVM model makes for not extending Meta: a faithful API mirror and a neutral spine have different fidelity contracts, and the neutral one should not inherit the mirror’s obligations.
What the relationship actually is. The GitHub model is a source, and the release model is a target. Load the organizations and repositories into a GitHub model with full fidelity, then project the parts the release model cares about into Repository and RepositoryStatistics nodes, joined by URL. Publishing a GitHub release runs the other way: the release model supplies tag, body and pre-release flag, the GitHub model and its client perform the call. Two directions, one projection each, neither model depending on the other’s shape.
One model or several. Start with one, covering organizations, repositories, releases and packages, which is the subset this effort actually needs. Split when a second area arrives that is a different domain rather than a bigger one, and workflows are that second area: Actions is CI execution, it pairs with a build or pipeline model rather than with repository metadata, and its object graph (workflows, runs, jobs, steps, artifacts) is larger than everything else combined. Planning for github and github-workflow as separate models from the start costs nothing and avoids the mono-model the micro-model rule exists to prevent. The GitLab model is the precedent for the shape; Xcore rather than generated-from-schema is the right authoring choice for the same reason it was there.
On generating it from GitHub’s OpenAPI description. Possible, and not recommended for the first cut. The description is enormous, the generated model would be lossy in the places that matter and faithful in the places that do not, and the subset needed here is small enough to hand author in the few days estimated. Revisit if the model ever needs to cover the API broadly.
Sequencing it with the release work is right, because it supplies both ends: the loader that populates the repository catalog from four organizations, and the client that creates releases. It also gives the release model its first real instance model, which is the thing that stops a metamodel being designed in anticipation of usage rather than from it.
Sequencing, and a note on the assessment
The assessment ruled: manifest and generator now, metamodel when it has more than one reader. This design overrides that, and the reason is the CalVer to SemVer switch. Once product versions stop tracking the release date, the mapping from release to product versions stops being computable and has to be recorded somewhere, and “somewhere” is either a YAML file that grows a second and third consumer within a month or the model it was going to become anyway.
What survives from the assessment and should not be relitigated: the orchestration fence, recording rather than computing SemVer, generating rather than maintaining the BOM, keeping the name release, and soft references to things that do not exist yet.
Editor’s note for Pavel: before any public writing built on this, verify the current state of release-please’s manifest mode and whether JReleaser has grown npm and VSIX support, since either would narrow the four-ecosystem gap claim. Also confirm the purl type for the Visual Studio Marketplace and Open VSX; if there is no registered type,
genericwith an explicitrepository_urlqualifier is the fallback and the readme should say so.
Relation to other Nasdanika work
Base classes come from NxCore, and nothing else is imported, which is deliberate: the model ships before the floors it will reference exist, the same way the JVM model does. The intended couplings, each a soft reference or a projection until the floor is there:
| Model | Relationship |
|---|---|
| Git | Repository.cloneUrl and Version.tag resolve into commits and tags |
| GitHub (proposed) | Loads the repository catalog, creates forge releases |
| Maven | Generated BOM and aggregator poms are Maven model instances; derived dependency edges come from resolved poms |
| JVM | Incompatibility.elementUri resolves to methods and types; a version publishes modules |
| Work | Work attaches to versions, and effort roll-up replaces Version.estimate |
| Capability | Value roll-up replaces Version.value |
| Lifecycle | Staged replaces ReleaseStatus |
| Cypher | The query surface: release order, estate footprint, compatibility findings |
The federated estate model the outline describes, every Nasdanika repository with its versions, dependencies, sizes and transitive sizes, is the intended first instance model and the intended demo of model rendering and querying. It is also a good corpus for the same reason a JVM graph is: real size, deep containment, heavy cross-referencing, and a shape that changes between releases.
Nasdanika Models