Publishing: signing keys and CI variables
release_and_publish publishes the tagged, validated release commit from the default-branch release pipeline to three artifact destinations, each needing its own signing material and CI/CD variables. After those publications succeed, the separately retryable trigger_update_user_projects job requests each configured consumer's own update pipeline. See CI and releases for the release flow.
Target | Gradle task | Needs |
|---|---|---|
Maven Central (Sonatype Central Portal) |
| Sonatype user token + an OpenPGP signing key |
Gradle Plugin Portal |
| a portal API key/secret + the same OpenPGP signing key |
JetBrains Marketplace (IDE plugin) |
| a plugin signing cert + a marketplace token |
Set every secret under Project → Settings → CI/CD → Variables, marked Protected and (where the value allows) Masked.
The release job currently expects this complete variable set:
Target | Required variables |
|---|---|
Maven Central |
|
Gradle Plugin Portal |
|
JetBrains Marketplace |
|
ORG_GRADLE_PROJECT_intellijPlatformPublishingToken is not required by STIG's current release job. The IDEA plugin build wires the Marketplace token from PUBLISH_TOKEN explicitly, so keeping both variables is harmless but PUBLISH_TOKEN is the one checked and consumed by release_and_publish.
Maven Central (Sonatype) + PGP signing
Artifacts must be signed with an OpenPGP key and uploaded with Sonatype Central Portal credentials. The build uses the Vanniktech plugin with in-memory signing.
The shared stig-gradle-plugin/gradle/publication-signing.gradle.kts configuration requires signing for every Maven and Gradle Plugin Portal publication, including snapshots and local Maven publication. It uses the existing signingInMemoryKey, signingInMemoryKeyPassword, and optional signingInMemoryKeyId Gradle properties for both destinations. Ordinary compilation and tests do not require a private key. The verifier implementation belongs to stig-gradle-plugin/src/repositoryChecks; it has no dependency on STIG's main plugin classes or native analyzer and can run while publishing STIG itself.
Verify publications before upload
This task builds and signs the publications, then verifies every detached .asc signature against the exact artifact bytes and the pinned STIG release key. It covers binary, source and Javadoc JARs, POMs, Gradle module metadata, and the Gradle plugin marker POM. It also rejects generated POMs with an empty or missing project name or description. Define those fields in the module's mavenPublishing.pom configuration or its POM_NAME and POM_DESCRIPTION properties. It does not upload anything. Provide the signing credentials as for publication and install GnuPG 2 (gpg) on a local publishing host; the CI toolchain image installs it explicitly.
publishPlugins, Maven repository publication tasks, and Maven Central preparation depend on the same verification gate, including when invoked directly. Missing signatures, altered artifacts, unexpected signing keys, expired keys or signatures, and revoked keys stop publication. Verification runs on every invocation, including when Gradle reuses its configuration cache. It uses an isolated temporary public keyring, disables key retrieval, and ignores the host's GnuPG configuration. JetBrains archives retain their separate certificate-based verification.
The root gradle.properties sets mavenCentralDeploymentValidation=PUBLISHED, so the Vanniktech publisher waits for Central Portal to finish publishing the deployment. CI completes this wait before uploading Gradle plugins or updating the example projects, which need the released artifacts to be downloadable. JetBrains Marketplace publication follows the Gradle Plugin Portal upload.
The main stig-gradle-plugin module also publishes to Maven Central. Its publication includes stig-gradle-plugin-<version>-verification.zip and the detached signature used by the consumer updater before Gradle loads STIG. A successful Plugin Portal upload does not establish that this bundle is available. Verify the bundle and its .asc URL on Maven Central before directing consumers to stigUpdate for that release.
The checked-in public key is gradle/signing/stig-release-key.asc. Its required primary-key fingerprint, also stored in stig-release-key.fingerprint, is:
This is the key that verifies the published ai.exida:stig-core:2.2.0 POM signature, checked when this policy was introduced. The public certificate was obtained from the Ubuntu keyserver by its full fingerprint; it belongs to Piotr Serwa <pserwa@exida.com>. Signing with a valid subkey bound to this pinned primary key is supported. Key rotation requires reviewing and updating both the public certificate and fingerprint in the repository, followed by a signed-publication check with the new private key. Downloading an arbitrary replacement key is not approval to trust it. Private keys and passphrases remain outside the repository.
This gate protects STIG's outgoing publications. Consumer dependency verification and updater-managed trust metadata are separate configuration; enabling this gate does not change their verification policy.
Generate a PGP key
You'll be prompted for a key passphrase (recommended; 12+ chars, your choice of characters — you store it). It protects the private key in your local keyring (~/.gnupg) and is also required in CI to unlock the key for signing. The script also writes an encrypted backup of the private key — keep that backup somewhere safe (offline / password manager); ~/.gnupg is the only copy of the key material otherwise, and losing it means you can't publish future releases.
CI/CD variables
Variable | What it is | What to paste |
|---|---|---|
| username part of a Sonatype Central User Token (not your web login) | the token's Username |
| password part of the same token | the token's Password (you usually can't retrieve it later) |
| your ASCII-armored private key, base64'd (see below) | output of the export+base64 command |
| the key passphrase from above | that passphrase (omit only if the key has none) |
| selects the signing subkey when needed | its eight-digit key ID accepted by Gradle; verification still checks the full primary fingerprint |
Get the Sonatype token from the Central Portal → Account / User Token.
Why base64 for the key (masking)
GitLab masked variables must be single-line and restricted-charset; an ASCII-armored PGP key is multiline, so masking fails. Store it base64-encoded:
release_and_publish decodes it back into ORG_GRADLE_PROJECT_signingInMemoryKey at runtime. Keep ORG_GRADLE_PROJECT_signingInMemoryKeyPassword as a normal masked variable.
Gradle Plugin Portal
Register at the Gradle Plugin Portal and create an API key + secret.
Set
GRADLE_PUBLISH_KEYandGRADLE_PUBLISH_SECRET(Masked, Protected).Supply the same OpenPGP signing credentials used for Maven Central. The shared signing convention applies Gradle's
signingplugin and signs both the implementation publication and the plugin marker.
See Publishing Gradle plugins and the Gradle Signing Plugin.
JetBrains Marketplace (IDE plugin)
buildPlugin creates an unsigned development ZIP. signPlugin creates the Marketplace ZIP next to it with the -signed.zip suffix, and publishIdea uploads only that verified signed ZIP. The STIG build overrides :stig-idea-plugin:publishPlugin to depend on verifyPluginSignature; if the signing key, certificate chain, passphrase, or signed ZIP is missing, publishing fails before upload instead of falling back to the unsigned buildPlugin archive.
Generate the first three signing variables with scripts/create-jetbrains-signing-keys.sh (it prints step-by-step instructions for the variables page); the publish token comes from your JetBrains tokens page.
The build decodes the encrypted private key into an owner-readable/writable file for signPlugin, then deletes it after signing, including when signing fails. It passes only the file path to the signer so Gradle's --info logs do not contain the private key. The key-writing task is excluded from state tracking and the build cache.
Variable | What it is |
|---|---|
| plugin signing private key |
| the matching certificate chain |
| passphrase for |
| JetBrains Marketplace upload token |
The native IntelliJ Gradle property-style variable ORG_GRADLE_PROJECT_intellijPlatformPublishingToken can remain in GitLab, but it is redundant for STIG releases unless the build is changed to use the IntelliJ plugin's default publishing-token convention again.
Marketplace screenshots and video
JetBrains Marketplace media is listing metadata, not IntelliJ plugin-manifest content. After a plugin version has been uploaded, maintain the gallery in Marketplace Admin → Media. Do not add screenshot or video fields to plugin.xml: JetBrains does not define such manifest elements.
The sanitized source images for that manual upload live in stig-docs/src/main/kotlin/com/stigway/images/ideaTour/. Keep their repository filenames descriptive and upload the canonical dark-theme screenshots in the same order as the IntelliJ IDEA tour. The source capture containing personal email/OpenPGP data and the duplicate capture are intentionally excluded.
Marketplace accepts a YouTube URL for its video field and recommends a demo shorter than five minutes; it does not accept the repository's MP4 file directly. Therefore the generated stig-idea-tour.mp4 is embedded in Writerside, while the Marketplace listing uses the canonical screenshots. Publishing the same video to Marketplace would first require a separately authorized YouTube upload and is not part of publishIdea.
release_and_publish fails fast with a clear message naming any missing variable, then runs plugin archive, structure, project-configuration, and signature verification checks before calling publishIdea. A direct :stig-idea-plugin:publishPlugin invocation has the same signed-archive requirement.
Consumer updates
After artifact publication succeeds, trigger_update_user_projects dispatches independent update pipelines using the project trigger tokens in the protected, masked STIG_USER_PROJECTS CI/CD variable. Each request supplies the typed inputs update_project=true and release_version=RELEASE_VERSION. The job completes when GitLab accepts the requests; consumer results remain in their own pipelines. Retry a rejected dispatch without republishing STIG.
Each consumer runs stigUpdate --yes, commits its managed dependency changes, and pushes with its own CI job token. The public examples build and publish Pages in that same update job. Enable Allow Git push requests to the repository for each consumer's own job token and grant the trigger token's owner permission to push its default branch. See the release flow for dispatch configuration, consumer setup, and retry and downgrade rules.
See also
CI and releases — the pipeline + the release trigger flow.
.gitlab-ci.ymlrelease_and_publish— the authoritative list of consumed variables.