diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ac82f8215..dbe21dafa 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,13 +9,14 @@ 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 +# release key, and the IntelliJ plugin goes to the JetBrains Marketplace, verified against the IDEs it +# 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 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 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) 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: @@ -270,6 +271,45 @@ 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: Upload the plugin + run: ./gradlew :open-java-format-idea-plugin:publishPlugin + env: + JETBRAINS_MARKETPLACE_TOKEN: ${{ secrets.JETBRAINS_MARKETPLACE_TOKEN }} + # 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..3f89f0094 100644 --- a/mise.toml +++ b/mise.toml @@ -84,4 +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 +apply JETBRAINS_MARKETPLACE_TOKEN 'op://Private/open-java-format/GitHub/JETBRAINS_MARKETPLACE_TOKEN' actions """ diff --git a/open-java-format-idea-plugin/build.gradle b/open-java-format-idea-plugin/build.gradle index 8c0901f8e..3e5e4cb0e 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,38 @@ 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. 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 { - // Inert until the token is present; wiring the marketplace release is Phase 2 work. token = providers.environmentVariable('JETBRAINS_MARKETPLACE_TOKEN') } + + // 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