Skip to content

Commit e673437

Browse files
committed
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.
1 parent a427d59 commit e673437

3 files changed

Lines changed: 101 additions & 7 deletions

File tree

‎.github/workflows/release.yml‎

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,16 @@ name: Release
99
# fails publishes nothing, and its deployments can be dropped in the Portal.
1010
#
1111
# Once Maven Central serves both, the Gradle plugins go to the Gradle Plugin Portal, signed with the same
12-
# release key. After publishing, a draft GitHub release on the tag collects the runnable jar, the Gradle
13-
# and IDE plugins and the native binaries.
12+
# release key, and the IntelliJ plugin goes to the JetBrains Marketplace, verified against the IDEs it
13+
# supports and signed with a key of its own. After publishing, a draft GitHub release on the tag collects the
14+
# runnable jar, the Gradle and IDE plugins and the native binaries.
1415
#
15-
# Needs six repository secrets: JRELEASER_MAVENCENTRAL_USERNAME and JRELEASER_MAVENCENTRAL_PASSWORD (the
16-
# Central Portal user token), JRELEASER_GPG_SECRET_KEY and JRELEASER_GPG_PASSPHRASE, and
17-
# GRADLE_PUBLISH_KEY and GRADLE_PUBLISH_SECRET (the Gradle Plugin Portal key). The draft release uses the
18-
# workflow's own GITHUB_TOKEN.
16+
# Needs ten repository secrets: JRELEASER_MAVENCENTRAL_USERNAME and JRELEASER_MAVENCENTRAL_PASSWORD (the
17+
# Central Portal user token), JRELEASER_GPG_SECRET_KEY and JRELEASER_GPG_PASSPHRASE, GRADLE_PUBLISH_KEY and
18+
# GRADLE_PUBLISH_SECRET (the Gradle Plugin Portal key), JETBRAINS_MARKETPLACE_TOKEN (a permanent token of
19+
# the Marketplace account that owns the plugin), and JETBRAINS_CERTIFICATE_CHAIN, JETBRAINS_PRIVATE_KEY and
20+
# JETBRAINS_PRIVATE_KEY_PASSWORD (the plugin signing key; without them the zip goes up unsigned). The draft
21+
# release uses the workflow's own GITHUB_TOKEN.
1922
on:
2023
push:
2124
tags:
@@ -270,6 +273,48 @@ jobs:
270273
JRELEASER_GPG_SECRET_KEY: ${{ secrets.JRELEASER_GPG_SECRET_KEY }}
271274
JRELEASER_GPG_PASSPHRASE: ${{ secrets.JRELEASER_GPG_PASSPHRASE }}
272275

276+
# The IntelliJ plugin carries the formatter jars inside its zip, so it does not wait for Maven Central. It
277+
# waits for the publish job like everything else, so that a version whose jars or native images failed
278+
# stays off the Marketplace too. The plugin is checked against the IDEs it declares support for before it
279+
# goes up; a plugin that fails there would fail JetBrains' own check after the upload anyway. The
280+
# Marketplace never takes a version back and rejects a version it already has, so a re-run of this job
281+
# fails once the upload went through.
282+
jetbrains-plugin:
283+
name: JetBrains Marketplace
284+
needs: publish
285+
runs-on: ubuntu-latest
286+
timeout-minutes: 60
287+
steps:
288+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
289+
with:
290+
fetch-depth: 0
291+
292+
- name: Install JDK 21
293+
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
294+
with:
295+
distribution: temurin
296+
java-version: '21'
297+
298+
- name: Verify the plugin against the IDEs it supports
299+
run: ./gradlew :open-java-format-idea-plugin:verifyPlugin
300+
301+
- name: Keep the verifier's report
302+
if: ${{ failure() }}
303+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
304+
with:
305+
name: plugin-verifier-report
306+
path: open-java-format-idea-plugin/build/reports/pluginVerifier
307+
if-no-files-found: ignore
308+
retention-days: 7
309+
310+
- name: Sign and upload the plugin
311+
run: ./gradlew :open-java-format-idea-plugin:publishPlugin
312+
env:
313+
JETBRAINS_MARKETPLACE_TOKEN: ${{ secrets.JETBRAINS_MARKETPLACE_TOKEN }}
314+
JETBRAINS_CERTIFICATE_CHAIN: ${{ secrets.JETBRAINS_CERTIFICATE_CHAIN }}
315+
JETBRAINS_PRIVATE_KEY: ${{ secrets.JETBRAINS_PRIVATE_KEY }}
316+
JETBRAINS_PRIVATE_KEY_PASSWORD: ${{ secrets.JETBRAINS_PRIVATE_KEY_PASSWORD }}
317+
273318
# What Maven Central does not carry — the runnable formatter jar, the Gradle plugins' jar, the IntelliJ
274319
# plugin zip, the Eclipse plugin jar, and every platform's native binary as a plain download — goes into
275320
# a draft GitHub release on the tag, each file signed with the release key. Only once both deployments

‎mise.toml‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,4 +84,13 @@ apply JRELEASER_GPG_SECRET_KEY 'op://Private/open-java-format/GitHub/JRELEASER_G
8484
apply JRELEASER_GPG_PASSPHRASE 'op://Private/open-java-format/GitHub/JRELEASER_GPG_PASSPHRASE' actions
8585
apply GRADLE_PUBLISH_KEY 'op://Private/open-java-format/GitHub/GRADLE_PUBLISH_KEY' actions
8686
apply GRADLE_PUBLISH_SECRET 'op://Private/open-java-format/GitHub/GRADLE_PUBLISH_SECRET' actions
87+
88+
# What release.yml uploads the IntelliJ plugin to the JetBrains Marketplace with: a permanent token of the
89+
# Marketplace account that owns the plugin, and the key the zip is signed with before the Marketplace adds
90+
# its own signature. The chain and the key are the PEM files as they are, line breaks included; the key
91+
# stays encrypted with the password.
92+
apply JETBRAINS_MARKETPLACE_TOKEN 'op://Private/open-java-format/GitHub/JETBRAINS_MARKETPLACE_TOKEN' actions
93+
apply JETBRAINS_CERTIFICATE_CHAIN 'op://Private/open-java-format/GitHub/JETBRAINS_CERTIFICATE_CHAIN' actions
94+
apply JETBRAINS_PRIVATE_KEY 'op://Private/open-java-format/GitHub/JETBRAINS_PRIVATE_KEY' actions
95+
apply JETBRAINS_PRIVATE_KEY_PASSWORD 'op://Private/open-java-format/GitHub/JETBRAINS_PRIVATE_KEY_PASSWORD' actions
8796
"""

‎open-java-format-idea-plugin/build.gradle‎

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
import org.jetbrains.intellij.platform.gradle.IntelliJPlatformType
12
import org.jetbrains.intellij.platform.gradle.TestFrameworkType
3+
import org.jetbrains.intellij.platform.gradle.models.ProductRelease
4+
import org.jetbrains.intellij.platform.gradle.tasks.VerifyPluginTask
25

36
/*
47
* Copyright 2017 Google Inc. All Rights Reserved.
@@ -100,10 +103,47 @@ intellijPlatform {
100103
}
101104
}
102105

106+
// publishPlugin uploads the zip to the JetBrains Marketplace, from the release workflow (see
107+
// .github/workflows/release.yml). The token is a permanent token of the Marketplace account that owns
108+
// the plugin; without it the task fails before uploading anything. The Marketplace never takes a version
109+
// back and rejects a version it already has.
103110
publishing {
104-
// Inert until the token is present; wiring the marketplace release is Phase 2 work.
105111
token = providers.environmentVariable('JETBRAINS_MARKETPLACE_TOKEN')
106112
}
113+
114+
// signPlugin runs before publishPlugin and signs the zip with this key, and the Marketplace then adds
115+
// its own signature on top. Without the three variables signPlugin is skipped and the unsigned zip goes
116+
// up, which the Marketplace accepts as well. The values are the PEM files themselves; the private key
117+
// stays encrypted, and the password decrypts it.
118+
signing {
119+
certificateChain = providers.environmentVariable('JETBRAINS_CERTIFICATE_CHAIN')
120+
privateKey = providers.environmentVariable('JETBRAINS_PRIVATE_KEY')
121+
password = providers.environmentVariable('JETBRAINS_PRIVATE_KEY_PASSWORD')
122+
}
123+
124+
// verifyPlugin checks the plugin's bytecode against real IDEs, which it downloads. Two of them: the
125+
// IDE the plugin is built against, which is the oldest one sinceBuild admits, and the newest IntelliJ
126+
// IDEA release. The default, recommended(), would take every major version in between, which is a
127+
// gigabyte per IDE for nothing the two ends do not show, and it stops at 2025.2: since 2025.3 IntelliJ
128+
// IDEA is one product (IntellijIdea), no longer split into Community and Ultimate.
129+
pluginVerification {
130+
// Fail on what would break an installation: classes or methods the IDE no longer has, a dependency
131+
// it lacks, an invalid descriptor. Usages of internal, deprecated or experimental API stay warnings
132+
// in the report: 2026.2 marked every way of looking a plugin descriptor up as @ApiStatus.Internal,
133+
// PluginManager.findEnabledPlugin included, and a warning about that must not hold a release.
134+
failureLevel = [
135+
VerifyPluginTask.FailureLevel.COMPATIBILITY_PROBLEMS,
136+
VerifyPluginTask.FailureLevel.MISSING_DEPENDENCIES,
137+
VerifyPluginTask.FailureLevel.INVALID_PLUGIN,
138+
]
139+
ides {
140+
current()
141+
latest {
142+
it.types.set([IntelliJPlatformType.IntellijIdea])
143+
it.channels.set([ProductRelease.Channel.RELEASE])
144+
}
145+
}
146+
}
107147
}
108148

109149
// This task resolves runtimeClasspath without telling Gradle it depends on it, so the dependent

0 commit comments

Comments
 (0)