STIG Help

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)

publishMaven

Sonatype user token + an OpenPGP signing key

Gradle Plugin Portal

publishGradle

a portal API key/secret + the same OpenPGP signing key

JetBrains Marketplace (IDE plugin)

publishIdea

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

ORG_GRADLE_PROJECT_mavenCentralUsername, ORG_GRADLE_PROJECT_mavenCentralPassword, SIGNING_KEY_B64, ORG_GRADLE_PROJECT_signingInMemoryKeyPassword

Gradle Plugin Portal

GRADLE_PUBLISH_KEY, GRADLE_PUBLISH_SECRET, SIGNING_KEY_B64, ORG_GRADLE_PROJECT_signingInMemoryKeyPassword

JetBrains Marketplace

PRIVATE_KEY, CERTIFICATE_CHAIN, PRIVATE_KEY_PASSWORD, PUBLISH_TOKEN

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

./gradlew verifyPublicationSignatures --console=plain --info

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:

F553 DB7E 59A6 E6B5 7819 26E5 FA09 3B76 01D3 F8E0

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

./scripts/generate_new_pgp.sh

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

ORG_GRADLE_PROJECT_mavenCentralUsername

username part of a Sonatype Central User Token (not your web login)

the token's Username

ORG_GRADLE_PROJECT_mavenCentralPassword

password part of the same token

the token's Password (you usually can't retrieve it later)

SIGNING_KEY_B64

your ASCII-armored private key, base64'd (see below)

output of the export+base64 command

ORG_GRADLE_PROJECT_signingInMemoryKeyPassword

the key passphrase from above

that passphrase (omit only if the key has none)

ORG_GRADLE_PROJECT_signingInMemoryKeyId (optional)

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:

gpg --export-secret-keys --armor <KEY_ID> | base64 -w0 # → paste as SIGNING_KEY_B64

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

  1. Register at the Gradle Plugin Portal and create an API key + secret.

  2. Set GRADLE_PUBLISH_KEY and GRADLE_PUBLISH_SECRET (Masked, Protected).

  3. Supply the same OpenPGP signing credentials used for Maven Central. The shared signing convention applies Gradle's signing plugin 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

PRIVATE_KEY

plugin signing private key

CERTIFICATE_CHAIN

the matching certificate chain

PRIVATE_KEY_PASSWORD

passphrase for PRIVATE_KEY

PUBLISH_TOKEN

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

23 October 2026