Publishing¶
Maintainer notes. Consumers only need the install instructions.
One tag publishes one version everywhere:
| Registry | Packages | Secrets |
|---|---|---|
| Maven Central | liquidglass, liquidglass-material3, liquidglass-bom, and the Gradle plugin dev.shayxo.liquidglass (artifact liquidglass-gradle-plugin plus its plugin marker) |
MAVEN_CENTRAL_USERNAME, MAVEN_CENTRAL_PASSWORD, SIGNING_KEY, SIGNING_KEY_PASSWORD (set up since 0.1.0) |
| npm | @shayxo.dev/liquidglass, the <liquid-glass> web component; @shayxo.dev/liquidglass-react-native |
none: trusted publishing (from 0.2.2; 0.2.1 used a token) |
| NuGet | LiquidGlass, LiquidGlass.WinForms, LiquidGlass.Wpf, LiquidGlass.Avalonia |
none: trusted publishing |
| pub.dev | liquiglass, the Flutter package (ports/flutter) |
none: automated publishing (GitHub Actions OIDC), once the owner's first upload exists |
| GitHub | a release with the notes in .github/release-notes/vX.Y.Z.md |
none (the workflow's token) |
The Gradle plugin is on Maven Central rather than the Gradle Plugin Portal, under the same signing key.
plugins { id("dev.shayxo.liquidglass") } finds it through its plugin marker in any build that lists
mavenCentral() in pluginManagement, which the Android Studio and Kotlin Multiplatform templates
already do. Listing it on the Plugin Portal as well is optional and not set up.
The version¶
VERSION_NAME in the root gradle.properties is the one number. The Gradle builds read it, the
NuGet packages read it (ports/dotnet/Package.props), and the npm job stamps it into package.json
at publish time. The Flutter package writes it in ports/flutter/pubspec.yaml, with -SNAPSHOT spelt
-dev as Dart does (0.3.0-SNAPSHOT is 0.3.0-dev), and heads ports/flutter/CHANGELOG.md with it;
the script does both, and CI fails if the pubspec drifts. The repository keeps both npm packages (ports/npm, ports/react-native) at
"private": true and 0.0.0, so a stray npm publish from a checkout cannot go out. Change it with the script, which also rewrites the
.NET lock files that record it:
Locally¶
./gradlew publishToMavenLocal # the library, Material 3 and the BOM
./gradlew -p gradle-plugin publishToMavenLocal # the Gradle plugin and its marker
puts every artifact in ~/.m2/repository/dev/shayxo/liquidglass/. A consuming project adds
mavenLocal() to its repositories (and to pluginManagement for the plugin), restricted to this
group so that a stale ~/.m2 cannot shadow anything else:
// settings.gradle.kts, in pluginManagement.repositories and dependencyResolutionManagement.repositories
mavenLocal { content { includeGroupAndSubgroups("dev.shayxo") } }
No signing key is needed for this. Signing switches on only when a key is present.
Each release¶
python .github/scripts/set_version.py X.Y.Z.- Write
.github/release-notes/vX.Y.Z.md(the GitHub release body) and giveCHANGELOG.mdthe dated## X.Y.Zheading. - Update the install snippets that name a version or say "unreleased":
grep -rn "unreleased\|0\.1\.0" README.md docs/*.md. - Open a pull request. Its
Publishchecks run the whole release as a dry run: every package is built, packed and checked, nothing is uploaded. - Merge, wait for CI on
main, then tag the merge commit: .github/workflows/publish.ymlrefuses unless the tag matchesVERSION_NAME, the version is not a SNAPSHOT, the commit is onmainand passed CI there, and the release notes exist. Then each registry is its own job, and the GitHub release follows them. Maven Central shows the artifacts within about half an hour.- If one job fails (a missing secret or policy, an outage), fix the cause and run
gh run rerun <id> --failed. The others stay published; npm and NuGet skip a version that is already there. A rerun runs the workflow and sources as tagged, so a fix to either needs a new patch version: 0.2.0 reached Maven Central only (npm refused its package names, and the NuGet upload step had a path bug), and 0.2.1 shipped the same code everywhere. python .github/scripts/set_version.py X.Y+1.0-SNAPSHOTonmain.
A version with a pre-release suffix (0.3.0-beta.1) goes to npm under the next dist-tag and is
marked as a pre-release on GitHub; NuGet and Maven treat it as a pre-release by its suffix.
npm, once¶
- An account at npmjs.com with two-factor authentication.
- The packages live under the account's scope,
@shayxo.dev. The unscoped namesliquidglassandliquidglass-react-nativewere free on 19 October 2026, but npm refused them on publish as too similar toliquid-glassandliquid-glass-react-native.npm viewcannot see that rule, so a new unscoped name is only known to be accepted once it is published. - npm configures trusted publishing per package, so
only once a package exists. The first publish of a new package therefore needs a granular access
token (Account, Access Tokens, Generate New Token, Granular: read and write on packages, two-factor
bypass allowed) in the
NPM_TOKENsecret, and the npm job's Publish step needsenv: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}for that one run. 0.2.1 published both packages that way. - Then, for each package: Settings, Trusted Publisher, GitHub Actions; owner
shayann07, repositoryliquidglass, workflow filenamepublish.yml, no environment; tick Allow npm publish (without it the publisher may only stage a publish, and the workflow publishes directly); Set up connection. Under Publishing access choose Require two-factor authentication and disallow bypass 2fa tokens. Delete the token on npmjs.com andgh secret delete NPM_TOKEN --repo shayann07/liquidglass. Both packages were set up this way on 20 October 2026. - npm marks a new trusted publisher pending validation until a publish goes through it, within about a day. 0.2.2 was released to validate it.
Packages are published with provenance: npm shows that each version was built by this workflow from this repository.
The npm jobs run npm 11 (trusted publishing needs 11.5.1 or later) with id-token: write; npm
exchanges the run's OIDC token for a one-time credential, so no npm secret exists.
pub.dev, once¶
The Flutter package is liquiglass. pub.dev refuses a new name that matches an existing one once
underscores are removed, so liquidglass is unavailable (liquid_glass exists), and so are
liquidglass_flutter and flutter_liquidglass.
pub.dev turns on automated publishing per package, so only once the package exists. The first upload
is therefore the owner's, by hand, with their Google account. Until then the pub-dev job stands down
with a warning, and the other registries publish as usual.
First upload (from main, any machine with Flutter 3.35 or later):
cd ports/flutterflutter pub publish --dry-runmust end withPackage has 0 warnings.A dirty git tree counts as a warning, so publish from a clean checkout.flutter pub publish. It prints ahttps://accounts.google.com/...link and waits.- Open the link, choose the Google account that should own the package, and click Allow (pub.dev asks to see your email address). The browser says Authorization complete.
- Back in the terminal, answer
yto Do you want to publish liquiglass ...?. The package appears at https://pub.dev/packages/liquiglass within a minute; its score takes a few minutes more.
Optional: a verified publisher. A badge with your domain instead of your email:
- https://pub.dev/create-publisher, enter
shayxo.dev, Create publisher. - pub.dev sends you to Google Search Console to prove you own the domain: add the DNS TXT record it shows at your DNS provider, wait for it to resolve, click Verify.
- https://pub.dev/packages/liquiglass/admin, Transfer to publisher, choose
shayxo.dev.
Automated publishing (after the first upload):
- https://pub.dev/packages/liquiglass/admin, section Automated publishing.
- Tick Enable publishing from GitHub Actions.
- Repository:
shayann07/liquidglass. Tag pattern:v{{version}}. - Leave Enable publishing with workflow_dispatch events unticked and Require GitHub Actions environment empty (the job uses none).
- Update.
From then on a tag vX.Y.Z publishes liquiglass X.Y.Z. dart-lang/setup-dart exchanges the run's
OIDC token (id-token: write) for a pub.dev credential, and pub.dev accepts it only for a tag push in
this repository whose tag matches the pattern and the pubspec version. The job skips a version pub.dev
already has.
NuGet, once¶
NuGet publishes through trusted publishing:
no API key is stored. The workflow signs in with NuGet/login, which exchanges the run's GitHub OIDC
token for an API key valid for an hour.
- An account at nuget.org (a Microsoft account); the workflow signs in as
shayxo.dev. - Account, Trusted Publishing, Create: package owner
shayxo.dev, repository ownershayann07, repositoryliquidglass, workflow filepublish.yml, no environment (set up on 19 October 2026). - The IDs
LiquidGlass,LiquidGlass.WinForms,LiquidGlass.WpfandLiquidGlass.Avaloniawere free on 19 October 2026; the first push claims them. Optionally reserve theLiquidGlassprefix afterwards (nuget.org ID prefix reservation), which marks the packages as verified.
Maven Central¶
Publishing goes through Sonatype's Central Portal, via the
vanniktech maven-publish plugin, for the
library build and for the separate gradle-plugin build alike.
Once (done for 0.1.0)¶
- An account at central.sonatype.com.
- A verified namespace. The group is
dev.shayxo.liquidglass, under the namespacedev.shayxo, which is verified once by proving ownership ofshayxo.dev: in the portal, Namespaces, Add Namespace, enterdev.shayxo; it hands you a verification key to publish as a DNS TXT record on the apex domain, then a Verify button. Every group under a verified namespace is covered, so future artifacts need nothing more. The Kotlin package remainscom.wexpa.liquidglass; Central checks the group, not the package. A group cannot be renamed after the first release without orphaning consumers, which is why this was settled first. - A user token from the portal (Account, then Generate User Token). This is the username and password the plugin uses, not the account login.
- A GPG key.
gpg --full-generate-key, then publish it:gpg --keyserver keyserver.ubuntu.com --send-keys <KEY_ID>. Export the secret key in the form the plugin reads:gpg --export-secret-keys --armor <KEY_ID>.
Credentials¶
Never in the repository. Locally, in ~/.gradle/gradle.properties:
mavenCentralUsername=<token username>
mavenCentralPassword=<token password>
signingInMemoryKey=<the armored secret key, newlines as \n>
signingInMemoryKeyPassword=<key passphrase>
In GitHub Actions, the same four as repository secrets named MAVEN_CENTRAL_USERNAME,
MAVEN_CENTRAL_PASSWORD, SIGNING_KEY and SIGNING_KEY_PASSWORD; the publish workflow maps
them onto ORG_GRADLE_PROJECT_* variables, which Gradle reads as project properties.
To publish from a machine instead of CI:
./gradlew publishAndReleaseToMavenCentral --no-configuration-cache
./gradlew -p gradle-plugin publishAndReleaseToMavenCentral --no-configuration-cache
publishToMavenCentral (without AndRelease) uploads and stops, leaving the deployment for a
manual release in the portal, which is the safer choice for a first attempt.
Notes from the first release¶
Each of these cost time once, on 2026-09-10, and should not cost it again.
- The signing passphrase must be plain ASCII. Gradle's signing plugin hands the passphrase
to BouncyCastle as Latin-1 while GnuPG derives the key from UTF-8, so a passphrase with any
character outside ASCII fails at
signAndroidPublicationwithPGPException: checksum mismatch in checksum of 20 bytes, even though gpg itself accepts it. Letters and digits only. Before pushing a tag, run the exact CI code path locally:and once it passes, feed the secrets from those same variables so nothing is retyped:$env:ORG_GRADLE_PROJECT_signingInMemoryKey = (gpg --export-secret-keys --armor KEY_ID | Out-String) $env:ORG_GRADLE_PROJECT_signingInMemoryKeyPassword = [System.Net.NetworkCredential]::new('', (Read-Host 'passphrase' -AsSecureString)).Password .\gradlew.bat :liquidglass:signAndroidPublication --no-configuration-cache$env:ORG_GRADLE_PROJECT_signingInMemoryKey | gh secret set SIGNING_KEY --repo shayann07/liquidglass. - Never export a key with PowerShell's
>. It writes UTF-16, which gpg cannot read back. Usegpg --export-secret-keys --armor --output key.asc KEY_ID. - On Windows, gpg ships inside Git:
Set-Alias gpg 'C:\Program Files\Git\usr\bin\gpg.exe', andgpg-connect-agentis in the same folder. GnuPG 2.4'skeyboxdcannot start when gpg is launched from PowerShell, so a fresh~/.gnupg/common.confneeds itsuse-keyboxdline commented out before the first key can be generated. - Namespace verification is a button, not a poll. Once the TXT record exists, use the
namespace row's menu, Verify Namespace. Check the record on both authoritative servers:
Dynadot's
ns1served it within minutes whilens2kept the old zone for over an hour on some of its anycast nodes, without bumping the zone serial, so any resolver that happened to askns2saw nothing. - GitHub started two publish runs for the v0.2.1 tag, three seconds apart, from one push. The
concurrency group queued the second until the first had published everything, but the registries
did not show the new version yet:
npm viewstill missed it and Central was still finishing the first deployment, so the second run failed (harmlessly) on every upload. Since 0.2.2 thecheckjob stands down when an earlier run for the same tag has succeeded. - A failed publish run can be rerun on the same tag with
gh run rerun <id> --failed. Nothing reaches Central until signing passes, so a signing failure leaves no partial deployment behind.