From e673437e0f38c443b702506389becac4e83e55da Mon Sep 17 00:00:00 2001 From: Alex Abashev Date: Fri, 25 Sep 2026 17:58:20 +0300 Subject: [PATCH 1/2] Publish the IntelliJ plugin to the JetBrains Marketplace on release A jetbrains-plugin job in release.yml, after publish like the Gradle plugins: verifyPlugin against the IDE the plugin is built against and the newest IntelliJ IDEA, then publishPlugin, which signs the zip when the signing secrets are present and uploads it with the Marketplace token. The zip carries the formatter jars, so the job does not wait for Maven Central. The plugin's build script gets the signing block, a verifier IDE set of two instead of recommended()'s four that stop at 2025.2, and a failure level of what breaks an installation: 2026.2 marks PluginManager.findEnabledPlugin, which the plugin calls, as internal API, and a warning about that must not hold a release. mise run gh:secrets sets the four new secrets from 1Password. --- .github/workflows/release.yml | 57 ++++++++++++++++++++--- mise.toml | 9 ++++ open-java-format-idea-plugin/build.gradle | 42 ++++++++++++++++- 3 files changed, 101 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ac82f8215..5ed5e57a9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,13 +9,16 @@ name: Release # fails publishes nothing, and its deployments can be dropped in the Portal. # # Once Maven Central serves both, the Gradle plugins go to the Gradle Plugin Portal, signed with the same -# release key. After publishing, a draft GitHub release on the tag collects the runnable jar, the Gradle -# and IDE plugins and the native binaries. +# release key, and the IntelliJ plugin goes to the JetBrains Marketplace, verified against the IDEs it +# supports and signed with a key of its own. After publishing, a draft GitHub release on the tag collects the +# runnable jar, the Gradle and IDE plugins and the native binaries. # -# Needs six repository secrets: JRELEASER_MAVENCENTRAL_USERNAME and JRELEASER_MAVENCENTRAL_PASSWORD (the -# Central Portal user token), JRELEASER_GPG_SECRET_KEY and JRELEASER_GPG_PASSPHRASE, and -# GRADLE_PUBLISH_KEY and GRADLE_PUBLISH_SECRET (the Gradle Plugin Portal key). The draft release uses the -# workflow's own GITHUB_TOKEN. +# Needs ten repository secrets: JRELEASER_MAVENCENTRAL_USERNAME and JRELEASER_MAVENCENTRAL_PASSWORD (the +# Central Portal user token), JRELEASER_GPG_SECRET_KEY and JRELEASER_GPG_PASSPHRASE, GRADLE_PUBLISH_KEY and +# GRADLE_PUBLISH_SECRET (the Gradle Plugin Portal key), JETBRAINS_MARKETPLACE_TOKEN (a permanent token of +# the Marketplace account that owns the plugin), and JETBRAINS_CERTIFICATE_CHAIN, JETBRAINS_PRIVATE_KEY and +# JETBRAINS_PRIVATE_KEY_PASSWORD (the plugin signing key; without them the zip goes up unsigned). The draft +# release uses the workflow's own GITHUB_TOKEN. on: push: tags: @@ -270,6 +273,48 @@ jobs: JRELEASER_GPG_SECRET_KEY: ${{ secrets.JRELEASER_GPG_SECRET_KEY }} JRELEASER_GPG_PASSPHRASE: ${{ secrets.JRELEASER_GPG_PASSPHRASE }} + # The IntelliJ plugin carries the formatter jars inside its zip, so it does not wait for Maven Central. It + # waits for the publish job like everything else, so that a version whose jars or native images failed + # stays off the Marketplace too. The plugin is checked against the IDEs it declares support for before it + # goes up; a plugin that fails there would fail JetBrains' own check after the upload anyway. The + # Marketplace never takes a version back and rejects a version it already has, so a re-run of this job + # fails once the upload went through. + jetbrains-plugin: + name: JetBrains Marketplace + needs: publish + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + + - name: Install JDK 21 + uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1 + with: + distribution: temurin + java-version: '21' + + - name: Verify the plugin against the IDEs it supports + run: ./gradlew :open-java-format-idea-plugin:verifyPlugin + + - name: Keep the verifier's report + if: ${{ failure() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: plugin-verifier-report + path: open-java-format-idea-plugin/build/reports/pluginVerifier + if-no-files-found: ignore + retention-days: 7 + + - name: Sign and upload the plugin + run: ./gradlew :open-java-format-idea-plugin:publishPlugin + env: + JETBRAINS_MARKETPLACE_TOKEN: ${{ secrets.JETBRAINS_MARKETPLACE_TOKEN }} + JETBRAINS_CERTIFICATE_CHAIN: ${{ secrets.JETBRAINS_CERTIFICATE_CHAIN }} + JETBRAINS_PRIVATE_KEY: ${{ secrets.JETBRAINS_PRIVATE_KEY }} + JETBRAINS_PRIVATE_KEY_PASSWORD: ${{ secrets.JETBRAINS_PRIVATE_KEY_PASSWORD }} + # What Maven Central does not carry — the runnable formatter jar, the Gradle plugins' jar, the IntelliJ # plugin zip, the Eclipse plugin jar, and every platform's native binary as a plain download — goes into # a draft GitHub release on the tag, each file signed with the release key. Only once both deployments diff --git a/mise.toml b/mise.toml index 126af7fa3..7da96f4d3 100644 --- a/mise.toml +++ b/mise.toml @@ -84,4 +84,13 @@ apply JRELEASER_GPG_SECRET_KEY 'op://Private/open-java-format/GitHub/JRELEASER_G apply JRELEASER_GPG_PASSPHRASE 'op://Private/open-java-format/GitHub/JRELEASER_GPG_PASSPHRASE' actions apply GRADLE_PUBLISH_KEY 'op://Private/open-java-format/GitHub/GRADLE_PUBLISH_KEY' actions apply GRADLE_PUBLISH_SECRET 'op://Private/open-java-format/GitHub/GRADLE_PUBLISH_SECRET' actions + +# What release.yml uploads the IntelliJ plugin to the JetBrains Marketplace with: a permanent token of the +# Marketplace account that owns the plugin, and the key the zip is signed with before the Marketplace adds +# its own signature. The chain and the key are the PEM files as they are, line breaks included; the key +# stays encrypted with the password. +apply JETBRAINS_MARKETPLACE_TOKEN 'op://Private/open-java-format/GitHub/JETBRAINS_MARKETPLACE_TOKEN' actions +apply JETBRAINS_CERTIFICATE_CHAIN 'op://Private/open-java-format/GitHub/JETBRAINS_CERTIFICATE_CHAIN' actions +apply JETBRAINS_PRIVATE_KEY 'op://Private/open-java-format/GitHub/JETBRAINS_PRIVATE_KEY' actions +apply JETBRAINS_PRIVATE_KEY_PASSWORD 'op://Private/open-java-format/GitHub/JETBRAINS_PRIVATE_KEY_PASSWORD' actions """ diff --git a/open-java-format-idea-plugin/build.gradle b/open-java-format-idea-plugin/build.gradle index 8c0901f8e..a2fe69783 100644 --- a/open-java-format-idea-plugin/build.gradle +++ b/open-java-format-idea-plugin/build.gradle @@ -1,4 +1,7 @@ +import org.jetbrains.intellij.platform.gradle.IntelliJPlatformType import org.jetbrains.intellij.platform.gradle.TestFrameworkType +import org.jetbrains.intellij.platform.gradle.models.ProductRelease +import org.jetbrains.intellij.platform.gradle.tasks.VerifyPluginTask /* * Copyright 2017 Google Inc. All Rights Reserved. @@ -100,10 +103,47 @@ intellijPlatform { } } + // publishPlugin uploads the zip to the JetBrains Marketplace, from the release workflow (see + // .github/workflows/release.yml). The token is a permanent token of the Marketplace account that owns + // the plugin; without it the task fails before uploading anything. The Marketplace never takes a version + // back and rejects a version it already has. publishing { - // Inert until the token is present; wiring the marketplace release is Phase 2 work. token = providers.environmentVariable('JETBRAINS_MARKETPLACE_TOKEN') } + + // signPlugin runs before publishPlugin and signs the zip with this key, and the Marketplace then adds + // its own signature on top. Without the three variables signPlugin is skipped and the unsigned zip goes + // up, which the Marketplace accepts as well. The values are the PEM files themselves; the private key + // stays encrypted, and the password decrypts it. + signing { + certificateChain = providers.environmentVariable('JETBRAINS_CERTIFICATE_CHAIN') + privateKey = providers.environmentVariable('JETBRAINS_PRIVATE_KEY') + password = providers.environmentVariable('JETBRAINS_PRIVATE_KEY_PASSWORD') + } + + // verifyPlugin checks the plugin's bytecode against real IDEs, which it downloads. Two of them: the + // IDE the plugin is built against, which is the oldest one sinceBuild admits, and the newest IntelliJ + // IDEA release. The default, recommended(), would take every major version in between, which is a + // gigabyte per IDE for nothing the two ends do not show, and it stops at 2025.2: since 2025.3 IntelliJ + // IDEA is one product (IntellijIdea), no longer split into Community and Ultimate. + pluginVerification { + // Fail on what would break an installation: classes or methods the IDE no longer has, a dependency + // it lacks, an invalid descriptor. Usages of internal, deprecated or experimental API stay warnings + // in the report: 2026.2 marked every way of looking a plugin descriptor up as @ApiStatus.Internal, + // PluginManager.findEnabledPlugin included, and a warning about that must not hold a release. + failureLevel = [ + VerifyPluginTask.FailureLevel.COMPATIBILITY_PROBLEMS, + VerifyPluginTask.FailureLevel.MISSING_DEPENDENCIES, + VerifyPluginTask.FailureLevel.INVALID_PLUGIN, + ] + ides { + current() + latest { + it.types.set([IntelliJPlatformType.IntellijIdea]) + it.channels.set([ProductRelease.Channel.RELEASE]) + } + } + } } // This task resolves runtimeClasspath without telling Gradle it depends on it, so the dependent From c4a837a0d97c26562bc16075038ff23b057500d4 Mon Sep 17 00:00:00 2001 From: Alex Abashev Date: Fri, 25 Sep 2026 18:15:34 +0300 Subject: [PATCH 2/2] Upload the IntelliJ plugin unsigned The Marketplace signs every upload with the JetBrains key, and the check of the author's key against the profile is not available yet, so a signature of our own buys nothing today. It can come back with a signing block in the plugin's build script and three secrets when it is wanted. --- .github/workflows/release.yml | 17 ++++++----------- mise.toml | 8 -------- open-java-format-idea-plugin/build.gradle | 13 ++----------- 3 files changed, 8 insertions(+), 30 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5ed5e57a9..dbe21dafa 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,15 +10,13 @@ name: Release # # Once Maven Central serves both, the Gradle plugins go to the Gradle Plugin Portal, signed with the same # release key, and the IntelliJ plugin goes to the JetBrains Marketplace, verified against the IDEs it -# supports and signed with a key of its own. After publishing, a draft GitHub release on the tag collects the -# runnable jar, the Gradle and IDE plugins and the native binaries. +# supports first. After publishing, a draft GitHub release on the tag collects the runnable jar, the Gradle +# and IDE plugins and the native binaries. # -# Needs ten repository secrets: JRELEASER_MAVENCENTRAL_USERNAME and JRELEASER_MAVENCENTRAL_PASSWORD (the +# Needs seven repository secrets: JRELEASER_MAVENCENTRAL_USERNAME and JRELEASER_MAVENCENTRAL_PASSWORD (the # Central Portal user token), JRELEASER_GPG_SECRET_KEY and JRELEASER_GPG_PASSPHRASE, GRADLE_PUBLISH_KEY and -# GRADLE_PUBLISH_SECRET (the Gradle Plugin Portal key), JETBRAINS_MARKETPLACE_TOKEN (a permanent token of -# the Marketplace account that owns the plugin), and JETBRAINS_CERTIFICATE_CHAIN, JETBRAINS_PRIVATE_KEY and -# JETBRAINS_PRIVATE_KEY_PASSWORD (the plugin signing key; without them the zip goes up unsigned). The draft -# release uses the workflow's own GITHUB_TOKEN. +# GRADLE_PUBLISH_SECRET (the Gradle Plugin Portal key) and JETBRAINS_MARKETPLACE_TOKEN (a permanent token of +# the Marketplace account that owns the plugin). The draft release uses the workflow's own GITHUB_TOKEN. on: push: tags: @@ -307,13 +305,10 @@ jobs: if-no-files-found: ignore retention-days: 7 - - name: Sign and upload the plugin + - name: Upload the plugin run: ./gradlew :open-java-format-idea-plugin:publishPlugin env: JETBRAINS_MARKETPLACE_TOKEN: ${{ secrets.JETBRAINS_MARKETPLACE_TOKEN }} - JETBRAINS_CERTIFICATE_CHAIN: ${{ secrets.JETBRAINS_CERTIFICATE_CHAIN }} - JETBRAINS_PRIVATE_KEY: ${{ secrets.JETBRAINS_PRIVATE_KEY }} - JETBRAINS_PRIVATE_KEY_PASSWORD: ${{ secrets.JETBRAINS_PRIVATE_KEY_PASSWORD }} # What Maven Central does not carry — the runnable formatter jar, the Gradle plugins' jar, the IntelliJ # plugin zip, the Eclipse plugin jar, and every platform's native binary as a plain download — goes into diff --git a/mise.toml b/mise.toml index 7da96f4d3..3f89f0094 100644 --- a/mise.toml +++ b/mise.toml @@ -84,13 +84,5 @@ apply JRELEASER_GPG_SECRET_KEY 'op://Private/open-java-format/GitHub/JRELEASER_G apply JRELEASER_GPG_PASSPHRASE 'op://Private/open-java-format/GitHub/JRELEASER_GPG_PASSPHRASE' actions apply GRADLE_PUBLISH_KEY 'op://Private/open-java-format/GitHub/GRADLE_PUBLISH_KEY' actions apply GRADLE_PUBLISH_SECRET 'op://Private/open-java-format/GitHub/GRADLE_PUBLISH_SECRET' actions - -# What release.yml uploads the IntelliJ plugin to the JetBrains Marketplace with: a permanent token of the -# Marketplace account that owns the plugin, and the key the zip is signed with before the Marketplace adds -# its own signature. The chain and the key are the PEM files as they are, line breaks included; the key -# stays encrypted with the password. apply JETBRAINS_MARKETPLACE_TOKEN 'op://Private/open-java-format/GitHub/JETBRAINS_MARKETPLACE_TOKEN' actions -apply JETBRAINS_CERTIFICATE_CHAIN 'op://Private/open-java-format/GitHub/JETBRAINS_CERTIFICATE_CHAIN' actions -apply JETBRAINS_PRIVATE_KEY 'op://Private/open-java-format/GitHub/JETBRAINS_PRIVATE_KEY' actions -apply JETBRAINS_PRIVATE_KEY_PASSWORD 'op://Private/open-java-format/GitHub/JETBRAINS_PRIVATE_KEY_PASSWORD' actions """ diff --git a/open-java-format-idea-plugin/build.gradle b/open-java-format-idea-plugin/build.gradle index a2fe69783..3e5e4cb0e 100644 --- a/open-java-format-idea-plugin/build.gradle +++ b/open-java-format-idea-plugin/build.gradle @@ -106,21 +106,12 @@ intellijPlatform { // publishPlugin uploads the zip to the JetBrains Marketplace, from the release workflow (see // .github/workflows/release.yml). The token is a permanent token of the Marketplace account that owns // the plugin; without it the task fails before uploading anything. The Marketplace never takes a version - // back and rejects a version it already has. + // back and rejects a version it already has. The zip goes up unsigned, and the Marketplace signs it with + // the JetBrains key; a signature of our own would take a `signing { }` block here and three more secrets. publishing { token = providers.environmentVariable('JETBRAINS_MARKETPLACE_TOKEN') } - // signPlugin runs before publishPlugin and signs the zip with this key, and the Marketplace then adds - // its own signature on top. Without the three variables signPlugin is skipped and the unsigned zip goes - // up, which the Marketplace accepts as well. The values are the PEM files themselves; the private key - // stays encrypted, and the password decrypts it. - signing { - certificateChain = providers.environmentVariable('JETBRAINS_CERTIFICATE_CHAIN') - privateKey = providers.environmentVariable('JETBRAINS_PRIVATE_KEY') - password = providers.environmentVariable('JETBRAINS_PRIVATE_KEY_PASSWORD') - } - // verifyPlugin checks the plugin's bytecode against real IDEs, which it downloads. Two of them: the // IDE the plugin is built against, which is the oldest one sinceBuild admits, and the newest IntelliJ // IDEA release. The default, recommended(), would take every major version in between, which is a