Provide plugin publishing workflow - #29
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #29 +/- ##
=========================================
Coverage 94.35% 94.35%
Complexity 84 84
=========================================
Files 9 9
Lines 744 744
Branches 99 99
=========================================
Hits 702 702
Misses 20 20
Partials 22 22 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
| --pinentry-mode loopback \ | ||
| --passphrase-fd 0 \ | ||
| --output "$HOME/.gradle/gradle.properties" \ | ||
| --decrypt .github/keys/gradle-plugin-portal.secret.properties.gpg |
There was a problem hiding this comment.
Must fix — .gitattributes has no rule for *.gpg, so Git normalizes line endings inside the credential ciphertext.
git check-attr -a .github/keys/gradle-plugin-portal.secret.properties.gpg returns nothing, so the blob falls back to Git's text auto-detection. Checking the committed bytes: the file is 179 bytes of GPG symmetrically encrypted data (AES256 cipher) and contains no NUL byte, so Git's heuristic classifies it as text. It does contain a lone LF at offset 171 (and a lone CR at offset 1).
On a checkout with core.autocrlf=true — the default on GitHub's windows-latest runners, which check.yml already uses, and on many Windows workstations — Git rewrites that lone LF to CRLF. The working-tree file becomes 180 bytes, so the OpenPGP symmetric-encrypted-data packet no longer matches its declared payload and this gpg --decrypt cannot succeed. (I verified the byte layout and the missing attribute directly; gpg is not installed here, so the decrypt failure itself is inferred from the byte change rather than observed.)
PROJECT.md states the cross-platform invariant this breaks: "Preserve cross-platform paths, permissions, line endings, and process behavior."
.gitattributes already declares *.jar binary. Add the same rule for the key:
*.gpg binary
Then confirm with git check-attr -a .github/keys/gradle-plugin-portal.secret.properties.gpg that it resolves to text: unset.
|
|
||
| - name: Check Build | ||
| shell: bash | ||
| run: ./gradlew :buildSrc:check check |
There was a problem hiding this comment.
Should fix — this check runs without the Java 17 toolchain that CI is required to provision.
buildSrc/src/main/kotlin/jvm-module.gradle.kts:90 pins the test launcher to BuildSettings.bytecodeVersion (17), and check.yml handles that by installing Java 17 first, exporting EMBED_CODE_JAVA_17_HOME, and passing -Dorg.gradle.java.installations.paths="$EMBED_CODE_JAVA_17_HOME". This job installs only Java 25, so test and functionalTest have no local JDK 17 and Gradle has to resolve one through api.foojay.io and 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 Publish run on this branch (run 30372596781) went straight from Set Up Gradle to Decrypt Gradle Plugin Portal Credentials, so the gap is untested.
Mirror check.yml: add the Set Up Java 17 for Compatibility Tests and Save Java 17 Toolchain steps before Set Up Java 25, and pass -Dorg.gradle.java.installations.paths="$EMBED_CODE_JAVA_17_HOME" on this invocation.
| --passphrase-fd 0 \ | ||
| --output "$HOME/.gradle/gradle.properties" \ | ||
| --decrypt .github/keys/gradle-plugin-portal.secret.properties.gpg | ||
| chmod 600 "$HOME/.gradle/gradle.properties" |
There was a problem hiding this comment.
Should fix — the decrypted credentials are world-readable until this chmod runs.
gpg --output creates the plaintext file with 0666 & ~umask, i.e. mode 0644 under the runner's default umask 022. The chmod 600 only closes that window afterwards, so the Portal key and secret exist at 0644 for the duration of the decryption.
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 umask 077 immediately before the gpg invocation (keeping this chmod is harmless), or pre-create the target with install -m 600 /dev/null "$HOME/.gradle/gradle.properties" and let gpg --yes write into it.
| } | ||
| } | ||
|
|
||
| rootProject.tasks.named<CheckVersionIncrement>("checkVersionIncrement") { |
There was a problem hiding this comment.
Should fix — pluginId has no value whenever :gradle-plugin is not configured.
checkVersionIncrement is registered in the root build.gradle.kts with pluginVersion and portalBaseUrl set but no value or convention for pluginId; this cross-project reach-back is the only assignment. Any invocation that configures the root project without this one leaves the guard's required input unset, so it fails on validation instead of querying the Portal. Reproduced on this branch:
$ ./gradlew --configure-on-demand :checkVersionIncrement
A problem was found with the configuration of task ':checkVersionIncrement' (type 'CheckVersionIncrement').
Value not set
Type 'io.spine.embedcode.gradle.publish.CheckVersionIncrement' property 'pluginId' doesn't have a configured value
./gradlew checkVersionIncrement passes today only because nothing enables configure-on-demand — the guard fails closed, but for the wrong reason and with a message that says nothing about publication. rootProject.tasks.named from a subproject is also cross-project state access, which Isolated Projects rejects, so this blocks that mode later.
Smallest fix: give the property a value where the task is registered — pluginId.convention("io.spine.embed-code") in the root script — and delete this block. If keeping the id declared exactly once matters more, register checkVersionIncrement in this module next to embedCodePlugin instead; both workflows invoke it unqualified, so ./gradlew checkVersionIncrement keeps working and nothing crosses a project boundary.
| } | ||
| } | ||
|
|
||
| gradlePlugin { |
There was a problem hiding this comment.
Nit — nothing in the build ties publishPlugins to the version guard.
checkVersionIncrement protects publication only because publish.yml happens to invoke it in an earlier step. A local ./gradlew :gradle-plugin:publishPlugins, or a later workflow edit that drops or reorders that step, republishes with no check at all.
If the task moves into this module as suggested on the rootProject.tasks.named block above, tasks.named("publishPlugins") { dependsOn(checkVersionIncrement) } would make the guard part of the publication contract rather than of CI step ordering. Non-blocking — the workflow does enforce it today.
Oleg-Melnik
left a comment
There was a problem hiding this comment.
@Vladyslav-Kuksiuk LGTM with comments to address.
This PR provides the plugin publishing workflow and initial publishing.
Also, provides version incrementation guard.
Resolves #3
Resolves #4