-
Notifications
You must be signed in to change notification settings - Fork 0
Provide plugin publishing workflow #29
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| name: Check Version Increment | ||
|
|
||
| on: | ||
| pull_request: | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| concurrency: | ||
| group: ${{ github.workflow }}-${{ github.ref }} | ||
| cancel-in-progress: true | ||
|
|
||
| jobs: | ||
| check-version: | ||
| name: Check Version Increment | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 10 | ||
|
|
||
| steps: | ||
| - name: Checkout Repository | ||
| uses: actions/checkout@v7 | ||
|
|
||
| - name: Set Up Java 25 | ||
| uses: actions/setup-java@v5 | ||
| with: | ||
| distribution: temurin | ||
| java-version: 25 | ||
|
|
||
| - name: Set Up Gradle | ||
| uses: gradle/actions/setup-gradle@v6 | ||
|
|
||
| - name: Check Plugin Version | ||
| run: ./gradlew checkVersionIncrement --stacktrace |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| name: Publish | ||
|
|
||
| on: | ||
| push: | ||
| branches: | ||
| - master | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| concurrency: | ||
| group: gradle-plugin-portal | ||
| cancel-in-progress: false | ||
|
|
||
| jobs: | ||
| publish: | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 15 | ||
|
|
||
| steps: | ||
| - name: Checkout Repository | ||
| uses: actions/checkout@v7 | ||
|
|
||
| - name: Set Up Java 25 | ||
| uses: actions/setup-java@v5 | ||
| with: | ||
| distribution: temurin | ||
| java-version: 25 | ||
|
|
||
| - name: Set Up Gradle | ||
| uses: gradle/actions/setup-gradle@v6 | ||
|
|
||
| - name: Check Plugin Version | ||
| shell: bash | ||
| run: ./gradlew checkVersionIncrement --stacktrace | ||
|
|
||
| - name: Check Build | ||
| shell: bash | ||
| run: ./gradlew :buildSrc:check check | ||
|
|
||
| - name: Decrypt Gradle Plugin Portal Credentials | ||
| shell: bash | ||
| env: | ||
| GRADLE_PORTAL_CREDENTIALS_KEY: ${{ secrets.GRADLE_PORTAL_CREDENTIALS_KEY }} | ||
| run: | | ||
| if [[ -z "$GRADLE_PORTAL_CREDENTIALS_KEY" ]]; then | ||
| echo "::error::The GRADLE_PORTAL_CREDENTIALS_KEY secret is unavailable." \ | ||
| "Grant this repository access to the organization secret." | ||
| exit 1 | ||
| fi | ||
|
|
||
| mkdir -p "$HOME/.gradle" | ||
| printf '%s' "$GRADLE_PORTAL_CREDENTIALS_KEY" | | ||
| gpg \ | ||
| --quiet \ | ||
| --batch \ | ||
| --yes \ | ||
| --pinentry-mode loopback \ | ||
| --passphrase-fd 0 \ | ||
| --output "$HOME/.gradle/gradle.properties" \ | ||
| --decrypt .github/keys/gradle-plugin-portal.secret.properties.gpg | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Must fix —
On a checkout with PROJECT.md states the cross-platform invariant this breaks: "Preserve cross-platform paths, permissions, line endings, and process behavior."
Then confirm with |
||
| chmod 600 "$HOME/.gradle/gradle.properties" | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should fix — the decrypted credentials are world-readable until this
PROJECT.md lists this as a trust boundary: "restrict the decrypted Gradle properties file to its owner". The current sequence satisfies the end state but not the invariant. Create the file restricted rather than fixing it up — either run |
||
|
|
||
| - name: Validate Plugin Portal Publication | ||
| shell: bash | ||
| run: ./gradlew :gradle-plugin:publishPlugins --validate-only | ||
|
|
||
| - name: Publish to Gradle Plugin Portal | ||
| shell: bash | ||
| run: ./gradlew :gradle-plugin:publishPlugins | ||
|
|
||
| - name: Remove Decrypted Credentials | ||
| if: ${{ always() }} | ||
| shell: bash | ||
| run: rm -f "$HOME/.gradle/gradle.properties" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,133 @@ | ||
| /* | ||
| * Copyright 2026, TeamDev. All rights reserved. | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * https://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Redistribution and use in source and/or binary forms, with or without | ||
| * modification, must retain the above copyright notice and the following | ||
| * disclaimer. | ||
| * | ||
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | ||
| * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | ||
| * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | ||
| * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | ||
| * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | ||
| * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | ||
| * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | ||
| * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | ||
| * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | ||
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||
| */ | ||
|
|
||
| package io.spine.embedcode.gradle.publish | ||
|
|
||
| import java.io.IOException | ||
| import java.net.URI | ||
| import java.net.http.HttpClient | ||
| import java.net.http.HttpClient.Redirect.NORMAL | ||
| import java.net.http.HttpRequest | ||
| import java.net.http.HttpResponse.BodyHandlers.discarding | ||
| import java.time.Duration | ||
| import org.gradle.api.DefaultTask | ||
| import org.gradle.api.GradleException | ||
| import org.gradle.api.provider.Property | ||
| import org.gradle.api.tasks.Input | ||
| import org.gradle.api.tasks.TaskAction | ||
| import org.gradle.api.tasks.UntrackedTask | ||
|
|
||
| /** | ||
| * Checks that the current plugin version is not published to the Gradle Plugin Portal. | ||
| * | ||
| * The task checks the exact plugin-marker POM. It succeeds when the Portal returns HTTP 404 and | ||
| * fails closed for an existing artifact, an unexpected response, or a network failure. | ||
| */ | ||
| @UntrackedTask(because = "The result depends on live Gradle Plugin Portal state.") | ||
| public abstract class CheckVersionIncrement : DefaultTask() { | ||
|
|
||
| /** Plugin ID whose marker artifact is checked. */ | ||
| @get:Input | ||
| public abstract val pluginId: Property<String> | ||
|
|
||
| /** Plugin version whose marker artifact is checked. */ | ||
| @get:Input | ||
| public abstract val pluginVersion: Property<String> | ||
|
|
||
| /** Base URL of the Gradle Plugin Portal. */ | ||
| @get:Input | ||
| public abstract val portalBaseUrl: Property<String> | ||
|
|
||
| /** Verifies that the current version can be published. */ | ||
| @TaskAction | ||
| public fun verifyVersion() { | ||
| val markerUri = markerUri() | ||
| when (val status = request(markerUri)) { | ||
| HTTP_OK -> | ||
| throw GradleException( | ||
| "Plugin `${pluginId.get()}` version `${pluginVersion.get()}` is already " + | ||
| "published to the Gradle Plugin Portal. Increment " + | ||
| "`embedCodePluginVersion` in `version.gradle.kts`.", | ||
| ) | ||
|
|
||
| HTTP_NOT_FOUND -> Unit | ||
| else -> | ||
| throw GradleException( | ||
| "Could not verify whether plugin `${pluginId.get()}` version " + | ||
| "`${pluginVersion.get()}` is already published. The Gradle Plugin Portal " + | ||
| "returned HTTP $status for `$markerUri`. Retry before publishing.", | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| private fun markerUri(): URI { | ||
| val id = pluginId.get() | ||
| val version = pluginVersion.get() | ||
| val markerArtifact = "$id.gradle.plugin" | ||
| val groupPath = id.replace('.', '/') | ||
| return URI.create( | ||
| "${portalBaseUrl.get().trimEnd('/')}/m2/$groupPath/$markerArtifact/" + | ||
| "$version/$markerArtifact-$version.pom", | ||
| ) | ||
| } | ||
|
|
||
| private fun request(markerUri: URI): Int { | ||
| val request = | ||
| HttpRequest | ||
| .newBuilder(markerUri) | ||
| .timeout(REQUEST_TIMEOUT) | ||
| .GET() | ||
| .build() | ||
| val client = | ||
| HttpClient | ||
| .newBuilder() | ||
| .connectTimeout(REQUEST_TIMEOUT) | ||
| .followRedirects(NORMAL) | ||
| .build() | ||
| try { | ||
| return client.send(request, discarding()).statusCode() | ||
| } catch (error: InterruptedException) { | ||
| Thread.currentThread().interrupt() | ||
| throw requestFailure(markerUri, error) | ||
| } catch (error: IOException) { | ||
| throw requestFailure(markerUri, error) | ||
| } | ||
| } | ||
|
|
||
| private fun requestFailure(markerUri: URI, cause: Exception): GradleException = | ||
| GradleException( | ||
| "Could not verify whether plugin `${pluginId.get()}` version " + | ||
| "`${pluginVersion.get()}` is already published. The Gradle Plugin Portal " + | ||
| "request to `$markerUri` failed. Retry before publishing.", | ||
| cause, | ||
| ) | ||
|
|
||
| private companion object { | ||
| const val HTTP_OK = 200 | ||
| const val HTTP_NOT_FOUND = 404 | ||
| val REQUEST_TIMEOUT: Duration = Duration.ofSeconds(30) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should fix — this
checkruns without the Java 17 toolchain that CI is required to provision.buildSrc/src/main/kotlin/jvm-module.gradle.kts:90pins the test launcher toBuildSettings.bytecodeVersion(17), andcheck.ymlhandles that by installing Java 17 first, exportingEMBED_CODE_JAVA_17_HOME, and passing-Dorg.gradle.java.installations.paths="$EMBED_CODE_JAVA_17_HOME". This job installs only Java 25, sotestandfunctionalTesthave no local JDK 17 and Gradle has to resolve one throughapi.foojay.ioand download it on every release run.PROJECT.md states the rule directly: "CI provisions its required JDKs before invoking Gradle. This build-time provisioning is outside the plugin's SHA-256 verification of Embed Code release assets." Routing the release build through an unpinned third-party toolchain download is exactly what that line exists to prevent, and it makes a foojay/CDN outage a release-blocking failure.
This step has also never actually executed in CI — the trial
Publishrun on this branch (run30372596781) went straight fromSet Up GradletoDecrypt Gradle Plugin Portal Credentials, so the gap is untested.Mirror
check.yml: add theSet Up Java 17 for Compatibility TestsandSave Java 17 Toolchainsteps beforeSet Up Java 25, and pass-Dorg.gradle.java.installations.paths="$EMBED_CODE_JAVA_17_HOME"on this invocation.