-
The pull request title drives the version number. Merges are squashed with
PR_TITLEas the commit message, andscripts/version.shderives the bump from conventional commits since the lastv<major>.<minor>.<patch>tag. A title with no recognized prefix silently yields a patch bump.Title prefix Bump feat!:, or aBREAKING CHANGE:footer line in the bodymajor feat:minor fix:,chore:,docs:, anything elsepatch The Kotlin conversion therefore ships under
feat!:to come out as 2.0.0. -
Versions: the git tag carries the
vprefix (v1.0.0), the Maven version does not (1.0.0). The prefix is not cosmetic —version.shrunsgit describe --match 'v[0-9]*.[0-9]*.[0-9]*'then strips thev; without it, every release would restart from 1.0.0. That glob, and the semver check that follows it, are what keep a tag likev.1.1.15orv1.2.08out of the arithmetic.
Two channels, two audiences.
| Channel | Contents | Auth to read | Published by |
|---|---|---|---|
| Maven Central | releases only | none | publishAggregationToCentralPortal |
| GitHub Packages | releases and snapshots | classic PAT | publish |
| GitHub Releases | the four plugin zips | none | pluginDistribution + gh release |
publish-snapshot is unchanged: a push to master publishes <next>-SNAPSHOT to GitHub
Packages and nothing else. publish-release runs both, Central first, then GitHub
Packages. The order is deliberate: GitHub Packages refuses to overwrite a release version it
already holds, so a Central failure after a successful GitHub Packages publish would leave an
untagged version behind and wedge the retry on the version number it recomputes. Central
first means a failure there leaves nothing published anywhere, and a re-run starts clean.
-
The Central path is
com.gradleup.nmcp, notcom.vanniktech.maven.publish. Both cover the Portal API; they differ in what they do to the publications. vanniktech creates the publication from a platform descriptor (JavaLibrary,KotlinJvm) and owns the artifact set, the sources and javadoc jars and the pom. nmcp creates nothing: it reads themaven-publishpublications the convention plugins already produce, stages them into a local repository and uploads the zip. That was decisive while the four shaded modules owned their own artifact set and pom; now that every module publishes the same way it is simply the smaller dependency, and there is no reason to change it. -
Only the nine library modules go to Central.
nmcpAggregation(project(…))in the root build names them one by one.schema-registry-examplespublishes to GitHub Packages as it always has, but it is not a library and Central is immutable — an artifact put there cannot be taken back. -
Signing is conditional on the key being present. Central rejects unsigned artifacts, so
gsr.publish-conventionsconfiguressigningwithuseInMemoryPgpKeys— but only whensigningInMemoryKeyresolves. A local./gradlew buildand a pull request build have no key, register noSigntask and need nothing. The release job supplies the key and every artifact, the pom and the module metadata get an.ascnext to them. -
Javadoc comes from Dokka. Central requires a javadoc jar; the sources are Kotlin, so
javadocJarpackages the output ofdokkaGeneratePublicationJavadoc. It is not wired intoassemble, so it costs nothing on a pull request build and is built on the publish path. -
The credentials are project properties, passed as
ORG_GRADLE_PROJECT_*. Gradle maps that environment prefix onto project properties by itself, so the workflow needs no glue.Repository secret Environment variable What it is MAVEN_CENTRAL_USERNAMEORG_GRADLE_PROJECT_mavenCentralUsernamePortal user token username MAVEN_CENTRAL_PASSWORDORG_GRADLE_PROJECT_mavenCentralPasswordPortal user token password SIGNING_IN_MEMORY_KEYORG_GRADLE_PROJECT_signingInMemoryKeyASCII-armored PGP private key SIGNING_IN_MEMORY_KEY_PASSWORDORG_GRADLE_PROJECT_signingInMemoryKeyPasswordits passphrase The account password is never one of them: the Portal issues a token pair for publishing.
-
A release waits for a human in the Portal.
publishingTypedefaults toUSER_MANAGED: the upload is validated automatically, then the deployment sits in Deployments until a maintainer presses Publish. Passing-PcentralPublishingType=AUTOMATICreleases without that click; the workflow does not, on purpose, and flipping it is a one-word change once a few releases have gone through. -
The bundle can be built and checked without publishing anything.
PACKAGE_VERSION=<version> ./gradlew nmcpZipAggregation nmcpCheckAggregationFiles
It stages every publication into
build/nmcp/zip/aggregation.zipand verifies that each coordinate carries the files Central requires. No credentials, no upload. -
A publication weighs 7.9 MB across 204 files, which is what the plugin distributions being release assets rather than Maven artifacts buys. Maven Central meters the free tier on release size — a 78 MB monthly threshold, evaluated as a three-month average and rate-limited from 1 October 2026 — and the four uber-jars this repository used to publish were 276 MB of a 284 MB release on their own. The reasoning, the measurements and the alternatives that were weighed are in build.md.
-
The plugin zips go to the GitHub Release, not to a Maven repository.
pluginDistributionis wired intoassemblebut not into any publication, sopublishandpublishAggregationToCentralPortalnever see it. The release job builds the four zips, covers them with the sameSHA256SUMS.txtand build-provenance attestation as the jars, and attaches them to the tag. Release assets are anonymous to download and are not metered, which is the whole point. -
They are built before either publish step, for the reason the Central-first order exists. Both publications are irreversible and the tag is written last, so a step that fails between them leaves a version published with no tag — and the retry recomputes the same number, which GitHub Packages then refuses to overwrite.
pluginDistributionneeds no credentials and can fail for ordinary build reasons, so it runs while a failure still costs nothing.
-
.mobsuccess.ymlis what mobsuccessbot enforces, and it holds no comments — the file is rewritten by the bot, so the reasoning lives here instead.- It disables the
linear,ms-testers,mobsuccess,closedandpythonworkflows: this repository does not require a Linear ticket per pull request. requiredApprovingReviewCount: 0onmasteris deliberate — an agent-driven repository, worked solo most days, and nobody can approve their own pull request: requiring one review would park every merge on a third party. The org-levelForce copilot reviewruleset still puts a Copilot review on each pull request, which reads the diff without gating the merge.isAdminEnforced: trueis what keeps that from being a hole. With no review required, an administrator could otherwise push straight tomaster, or merge over a red check: the required checks were the only gate, and admins were exempt from it. They are not any more. Set it in both places — the bot owns the value, so the file is what makes it durable, andPOST .../branches/master/protection/enforce_adminsis what makes it effective before the next policy run.additional-required-job-namesexists because automatic detection only knows the jobs of the policy's template workflows; those ofci.yml, written by hand, are invisible to it, and without the list a red CI still leaves a pull request mergeable. Set these here, never in the branch-protection UI — the next policy run overwrites the UI.Tests on the protobuf floorguards the version contract described in build.md: it is the only job that resolves protobuf at the floor the generated code imposes on consumers, so without it a floor raised past a consumer's own pin merges green.- The list names resolved job names, so the matrix appears as
Tests on JDK 21andTests on JDK 25. Both are required becauseGradle Buildruns on 17 only: they carry the guarantee that JSON Schema output does not depend on the JDK that produced it, and without them a regression visible only on 21 or 25 merges green. Analyze java-kotlinis required only becauseENABLE_CODEQLis set as a repository variable. The job is gated on that variable, and a required check that skips counts as green — so listing it while the variable is unset would look like a security gate without being one. Unset the variable and the entry has to go with it.ktlintwas advisory from the start — reviewdog reported atlevel: warningwithfail_on_errorunset, so the job was green whatever ktlint found, and it was not in the list either. It now runs atlevel: errorwithfail_on_error: true, and is required.
- It disables the
-
prodrequires the same five checks asmaster, and that is what makes the release gate real: without them, an administrator pushing a commit that never saw a pull request got a greenpublish-releaseand a tagged release out of it. The branch is not in.mobsuccess.yml— the bot only managesmaster— soprodis set throughPUT .../branches/prod/protectionand stays hand-held. -
The release push still works because a check run belongs to a commit, not to a branch.
master → prodis a fast-forward of a SHA that already carries the five green checks from its own push tomaster, and branch protection evaluates the SHA being pushed, so the gate is already satisfied when the push arrives. It matters that this is the mechanism:codeql.ymldoes not even trigger on a push toprod, and the release would deadlock waiting for a run that never starts if the check had to be produced on the branch. The one consequence to know is timing — pushing toprodbeforemaster's own post-merge run finishes leaves the checks pending and the push refused until they go green. -
prodtakesstrict: falsewheremastertakesstrict: true. "Up to date before merging" answers a questionproddoes not have: nothing is merged into it, it is fast-forwarded. Enabling it there would only add a way for the release push to be refused.
The build resolves third-party code, and CI runs it with a GitHub token. These rules keep
that surface small; none of them survive a careless rewrite, so check them before touching
.github/, settings.gradle.kts or the wrapper.
- GitHub Actions are pinned to a commit SHA, never a tag: a tag is mutable and moving
it is how
tj-actions/changed-fileswas compromised. The trailing# vX.Y.Zcomment is the human-readable part and Dependabot rewrites both together — do not replace the SHA with the tag. - The Gradle wrapper is checksummed twice:
distributionSha256Sumingradle-wrapper.propertiespins the distribution zip, andvalidate-wrappers: truechecks the committedgradle-wrapper.jaragainst Gradle's published hashes before any Gradle code runs. Both have to be refreshed together when the wrapper is upgraded, with the values fromservices.gradle.org/distributions/gradle-<version>-bin.zip.sha256. Every job that runs Gradle sets that flag — ongradle/actions/setup-gradleinci.yml,codeql.ymlandintegration.yml, ongradle/actions/dependency-submissionindependency-submission.yml, which takes the same option. A new job that runs./gradlewhas to set it too.integration.ymlhad neither: it ran Gradle offsetup-java's owncache: gradle, which restores a cache but validates nothing, and it referenced its four actions by tag. It was also the only workflow handingchecks: writeto a third-party action pinned to a moving tag. - The Gradle cache is written from a push to
masteronly.setup-gradlerestores it everywhere but takescache-read-onlyon anything else, so neither a pull request — including one from a fork — nor a push toprodcan poison the cache a latermasterbuild restores.test-jdkand the twopublish-*jobs are read-only unconditionally: they consume whatGradle Buildproduced. test-jdklists its JDKs newest-first for a reason.setup-javamakes the last entryJAVA_HOME, so the order25, 17, 21is what puts Gradle itself on 21 while keeping 17 as the compilation toolchain and leaving 25 available as a target to run the tests on. Reordering that list silently changes which JVM Gradle runs on.- No job that runs repository code holds a write token.
Gradle Buildruns third-party plugin code and is limited tocontents: readwithpersist-credentials: false, so a hostile dependency finds neither a token in.git/confignor one it could comment or push with. Keep the pull request comments inreport, which downloads artifacts and runs no repository code —reportneedscontents: readon top of its write scopes, sincepublish-unit-test-result-actionresolves the commit through the API.permissions: {}at the top of the workflow means a new job starts with nothing, since the repository-wide default is stillwrite. The exceptions are deliberate:ktlinttakespull-requests: readbecause reviewdog reads the diff to place its findings — it no longer takeschecks: write, see thegithub-pr-annotationsnote below — and the twopublish-*jobs needpackages: writeto publish — plus, forpublish-release,contents: writeand the only checkout that keeps its credentials persisted, since its last step pushes the release tag. Those two run on a push tomasterorprod, never on a pull request. reporttolerates a build that produced nothing. Its downloads arecontinue-on-error, and each publishing step is gated on its own download having succeeded: a compile failure leavesGradle Buildred on its own rather than dragging a second job down with it. The job itself runs on!cancelled(), so a failing test suite is still reported.- Dependabot proposes a release only once it has aged (
cooldownindependabot.yml): a hijacked publish is usually pulled within days. The cooldown covers version updates only — a security update still lands the day it is published, which is the intent. The schedule is monthly and the groups are wide, so the batch arrives as a handful of pull requests; a dependency lands in the first group it matches, which is why theminor-and-patchcatch-all comes last and why majors, excluded from it, keep an individual pull request. - The dependency graph is generated and submitted by two different jobs. Dependabot
alerts otherwise see only the direct versions of the catalog, never the transitives that
actually carry most CVEs. Submitting the graph needs
contents: write, and resolving it needs to run the build — sodependency-submission.ymlsplits the two, exactly asci.ymlsplitsbuildandreport:generateresolves the graph withcontents: readand uploads it as an artifact,submitposts that artifact and runs no repository code. - CodeQL builds with
--no-build-cache, and that flag is load-bearing.codeql.ymlanalysesjava-kotlinin manual build mode, which extracts code from the compiler invocations./gradlew assemblemakes. On a branch that touches no source, the restored Gradle build cache serves everycompileKotlin/compileJavaFROM-CACHE, no compiler ever runs, and the extractor fails the job on exit 32, having seen no source at all — which, sinceAnalyze java-kotlinis required, blocked every documentation-only pull request. Disabling the build cache for that one invocation is what makes the analysis real; the dependency cache is untouched, so the cost is a full compile (~10 min) rather than a full download. Skipping the workflow on docs-only branches would have been the wrong fix: a required check that skips counts as green, which is the anti-pattern.mobsuccess.ymldocuments. - CodeQL is still gated on the
ENABLE_CODEQLrepository variable. It is set now that the repository is public and has code scanning; the gate stays so the workflow can be turned off without editing it. dependency-review.ymlblocks a vulnerable dependency at the pull request, wheredependency-submission.ymlonly tells Dependabot what to alert on after the fact. It runs onpull_requestalone, holdscontents: readand nothing else — the action reads the dependency graph through the API and never needs to write — and takesfail-on-severity: high, so a high or critical advisory on a newly introduced dependency fails the job while the long tail stays with the Dependabot alerts that already cover the full transitive graph. It carries noENABLE_*gate, unlike CodeQL: a gate whose variable is unset makes the job skip, a skipped required check counts as green, and the result would be a security gate that is not one. The job is not inadditional-required-job-namesyet — promoting it is a one-line change to.mobsuccess.ymlonce it has a few pull requests of history.- Releases are attested and checksummed.
publish-releasebuilds aSHA256SUMS.txtover every jar it published and attaches it to the GitHub release, thenactions/attest-build-provenancesigns a provenance statement for those same jars. A consumer can check a jar pulled from Maven Central or GitHub Packages against the release checksums, or verify its build provenance withgh attestation verify <jar> --repo <this repo>. The job therefore also holdsid-token: writeandattestations: write. - Release tags are immutable, and the
Immutable release tagsruleset is what the attestation above rests on. A provenance statement binds a jar to the commit a tag pointed at; if the tag can be moved afterwards, the version a consumer resolves and the source that was attested drift apart without a trace. The ruleset targetsrefs/tags/v*withdeletionandnon_fast_forward, and deliberately omitscreation—publish-releasecreatesv<version>with theGITHUB_TOKEN, and rulesets apply to thegithub-actionsbot like anyone else, so blocking creation would break every release. There are no bypass actors: re-cutting a botched tag means flipping the ruleset'senforcementtodisabledfor as long as it takes, which is the audit trail the bypass would not leave. ktlintreports throughgithub-pr-annotations, and that is what makes it a gate. Thegithub-pr-checkreporter it used before opened a second check run of its own, also calledktlint, next to the job's — two checks under one name, which is no basis for a required context.github-pr-annotationswrites the findings to the job's own log instead, soktlintnames exactly one check and the job's exit code is the verdict. It also drops thechecks: writethe old reporter needed.filter_modeisnofilter, notfile: reviewdog resolves the changed files from the pull request diff, which does not exist on apushor amerge_grouprun, sofilewould have let a violation through the merge queue unseen.nofilterlints the whole tree on every event, which is only viable because the tree is clean — a violation left anywhere blocks every pull request until it is fixed.- Redundant runs are cancelled on pull requests only.
build,test-jdkandktlintcarry aconcurrencygroup keyed on the pull request number —test-jdkkeys on its matrix leg too, or the two legs would cancel each other — withcancel-in-progressscoped topull_request. A push tomasterorprodis never cancelled:publish-snapshotandpublish-releasehang offbuild, and cancelling it would cancel the publication with it.merge_groupis excluded for the same reason. RepositoriesMode.FAIL_ON_PROJECT_REPOSinsettings.gradle.ktsturns a module declaring its own repository into an error rather than a silent addition to the resolution order.- Dependabot is told to leave Flink alone. The catalog pins
flinkat 1.12.2 becauseflink-streaming-java_2.11exists under no later coordinate: the Scala suffix was dropped upstream. Since both Flink artifacts share the same version reference, any bump Dependabot proposed would either fail to resolve or dead-end at 1.14.6. Theignoreentry onorg.apache.flink:*keeps that pull request from being opened; moving off 1.12.2 is a deliberate migration, not a version bump. - Dependency locking is deliberately absent. The catalog fixes every direct version with
no range and Maven Central is immutable, so resolution is already deterministic and a
transitive only moves inside a reviewable commit. Lockfiles would add a manual
--write-locksto every bump, since Dependabot cannot regenerate them.gradle/verification-metadata.xmlis the control that would add something, and it is not wired yet.