ci(jetbrains): publish marketplace rc builds

This commit is contained in:
kirillk
2026-05-03 16:12:09 -04:00
parent 7d2cb5ed6e
commit b12c7cf10d
4 changed files with 290 additions and 2 deletions
+166
View File
@@ -0,0 +1,166 @@
name: publish-jetbrains
on:
push:
tags:
- "jetbrains/*"
concurrency:
group: publish-jetbrains-${{ github.ref }}
cancel-in-progress: false
permissions:
contents: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
publish:
if: github.repository == 'Kilo-Org/kilocode'
runs-on: blacksmith-8vcpu-ubuntu-2404
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "24"
- name: Setup Bun
uses: ./.github/actions/setup-bun
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
- name: Install build tools
run: |
sudo apt-get update
sudo apt-get install -y patchelf zip
- name: Validate version tag
id: version
run: |
tag="$GITHUB_REF_NAME"
if [[ "$tag" != jetbrains/* ]]; then
echo "Unsupported tag '$tag'. Expected jetbrains/<version>." >&2
exit 1
fi
version="${tag#jetbrains/}"
if [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+-rc\.[0-9]+$ ]]; then
{
echo "version=$version"
echo "kind=rc"
echo "marketplace_channel=eap"
echo "cli_channel=rc"
} >> "$GITHUB_OUTPUT"
exit 0
fi
if [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
{
echo "version=$version"
echo "kind=stable"
echo "marketplace_channel=default"
echo "cli_channel=latest"
} >> "$GITHUB_OUTPUT"
echo "Stable JetBrains Marketplace publishing is implemented but intentionally disabled; use an rc tag such as jetbrains/7.0.1-rc.1." >&2
exit 1
fi
echo "Unsupported JetBrains plugin version '$version'. Expected x.y.z-rc.n or x.y.z." >&2
exit 1
- name: Validate publishing secrets
run: |
missing=0
for name in JETBRAINS_MARKETPLACE_TOKEN JETBRAINS_CERTIFICATE_CHAIN JETBRAINS_PRIVATE_KEY JETBRAINS_PRIVATE_KEY_PASSWORD; do
if [[ -z "${!name}" ]]; then
echo "Missing required secret: $name" >&2
missing=1
fi
done
exit "$missing"
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 }}
- name: Prepare CLI resources
working-directory: packages/kilo-jetbrains
run: bun script/build.ts --production --prepare-cli
env:
KILO_VERSION: ${{ steps.version.outputs.version }}
KILO_CHANNEL: ${{ steps.version.outputs.cli_channel }}
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
- name: Verify plugin
working-directory: packages/kilo-jetbrains
run: ./gradlew verifyPlugin -Pproduction=true -Pkilo.channel="$CHANNEL"
env:
CHANNEL: ${{ steps.version.outputs.marketplace_channel }}
- name: Publish to JetBrains Marketplace
working-directory: packages/kilo-jetbrains
run: ./gradlew publishPlugin -Pproduction=true -Pkilo.channel="$CHANNEL"
env:
CHANNEL: ${{ steps.version.outputs.marketplace_channel }}
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 }}
- name: Resolve plugin archive
id: archive
run: |
mapfile -t signed < <(compgen -G "packages/kilo-jetbrains/build/distributions/*-signed.zip")
if [[ "${#signed[@]}" -eq 1 ]]; then
echo "path=${signed[0]}" >> "$GITHUB_OUTPUT"
exit 0
fi
if [[ "${#signed[@]}" -gt 1 ]]; then
echo "Expected exactly one signed JetBrains plugin ZIP, found ${#signed[@]}." >&2
printf '%s\n' "${signed[@]}" >&2
exit 1
fi
mapfile -t files < <(compgen -G "packages/kilo-jetbrains/build/distributions/*.zip")
if [[ "${#files[@]}" -ne 1 ]]; then
echo "Expected exactly one JetBrains plugin ZIP, found ${#files[@]}." >&2
printf '%s\n' "${files[@]}" >&2
exit 1
fi
echo "path=${files[0]}" >> "$GITHUB_OUTPUT"
- name: Upload to GitHub Release
run: |
tag="$GITHUB_REF_NAME"
title="JetBrains $VERSION"
notes="JetBrains plugin $VERSION."
if gh release view "$tag" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
gh release upload "$tag" "$ARCHIVE" --clobber --repo "$GITHUB_REPOSITORY"
exit 0
fi
gh release create "$tag" "$ARCHIVE" --title "$title" --notes "$notes" --prerelease --repo "$GITHUB_REPOSITORY"
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ steps.version.outputs.version }}
ARCHIVE: ${{ steps.archive.outputs.path }}
- name: Upload workflow artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: kilo-jetbrains-${{ steps.version.outputs.version }}
path: packages/kilo-jetbrains/build/distributions/*.zip
if-no-files-found: ignore
+23
View File
@@ -52,6 +52,29 @@ The built plugin archive is at `build/distributions/kilo.jetbrains-<version>.zip
---
## Publish release candidates
JetBrains release-candidate builds publish from tags matching `jetbrains/x.y.z-rc.n`, for example `jetbrains/7.0.1-rc.1`. The git tag is the source of truth for the Marketplace plugin version; production Gradle builds ignore manually supplied version properties and fail if `HEAD` is not tagged with `jetbrains/<version>`. Local dev builds can use the placeholder `0.0.0-dev` version.
The `publish-jetbrains` workflow publishes RC builds to the JetBrains Marketplace `eap` channel and uploads the same plugin ZIP to a GitHub prerelease for the tag. Stable tags like `jetbrains/x.y.z` are recognized by the workflow but intentionally rejected until default-channel publishing is enabled.
Testers can install or update EAP builds from this custom plugin repository:
```
https://plugins.jetbrains.com/plugins/eap/list
```
Required GitHub Actions secrets:
- `JETBRAINS_MARKETPLACE_TOKEN`
- `JETBRAINS_CERTIFICATE_CHAIN`
- `JETBRAINS_PRIVATE_KEY`
- `JETBRAINS_PRIVATE_KEY_PASSWORD`
Before the first publish, complete `RELEASE_TODO.md`.
---
## Run the plugin
Use the `runIde` Gradle task (available in the Gradle tool window or via the "Run JetBrains Plugin" run configuration) to launch a sandboxed IntelliJ instance with the plugin installed.
+44
View File
@@ -0,0 +1,44 @@
# JetBrains Release Todo
## One-Time Marketplace Setup
- Confirm `ai.kilocode.jetbrains` is the final permanent Marketplace plugin ID before the first upload.
- Build a production ZIP locally or in CI for the first Marketplace version.
- Manually upload the first plugin version in JetBrains Marketplace if the plugin has not been published before.
- Confirm the plugin is owned by the correct Kilo vendor or organization.
- Confirm the `eap` custom channel exists or is accepted on the first RC upload.
- Confirm Marketplace signing requirements before publishing RC builds.
## One-Time GitHub Setup
- Create a JetBrains Marketplace permanent token from Marketplace `My Tokens`.
- Add `JETBRAINS_MARKETPLACE_TOKEN` to GitHub Actions secrets or the protected environment.
- Confirm `GITHUB_TOKEN` has `contents: write` permission for creating and updating GitHub Releases from `jetbrains/*` tags.
- Optionally create a protected `jetbrains-marketplace` GitHub Environment with required reviewers.
- If using an environment, move the Marketplace and signing secrets there and set the workflow job environment.
## One-Time Signing Certificate Setup
- Generate or locate the JetBrains plugin signing private key and certificate chain.
- Add `JETBRAINS_CERTIFICATE_CHAIN` with the full certificate chain PEM content.
- Add `JETBRAINS_PRIVATE_KEY` with the private key PEM content.
- Add `JETBRAINS_PRIVATE_KEY_PASSWORD` with the private key password.
- Keep PEM files out of git, local logs, and workflow output.
- If GitHub secret input mangles multiline PEM values, store base64-encoded values and verify the Gradle signing task decodes them correctly.
## Per-RC Release
- Choose an RC version in the form `x.y.z-rc.n`.
- Push tag `jetbrains/x.y.z-rc.n`.
- Watch the `publish-jetbrains` workflow.
- Download and retain the workflow artifact if needed.
- Confirm the update appears on the JetBrains Marketplace `eap` channel.
- Confirm the GitHub Release for the `jetbrains/x.y.z-rc.n` tag exists and contains the JetBrains plugin ZIP asset.
- Share `https://plugins.jetbrains.com/plugins/eap/list` with testers.
## Stable Release Guard
- Stable tags like `jetbrains/x.y.z` are intentionally rejected for now.
- Before enabling stable releases, remove the workflow stable guard.
- Verify `kilo.channel=default` publishes to the default Marketplace channel.
- Update this checklist before stable releases are enabled.
+57 -2
View File
@@ -4,7 +4,32 @@ import org.jetbrains.intellij.platform.gradle.tasks.RunIdeTask
import org.jetbrains.intellij.platform.gradle.tasks.aware.SplitModeAware.SplitModeTarget
group = "ai.kilocode.jetbrains"
version = "7.0.1"
fun checked(value: String): String {
if (value == "0.0.0-dev") return value
require(Regex("^[0-9]+\\.[0-9]+\\.[0-9]+(-rc\\.[0-9]+)?$").matches(value)) {
"Invalid JetBrains plugin version: $value"
}
return value
}
fun gitTag(): String? {
val text = providers.exec {
commandLine("git", "tag", "--points-at", "HEAD")
}.standardOutput.asText.get()
return text.lineSequence().map { it.trim() }.firstOrNull { it.startsWith("jetbrains/") }
}
val release = providers.gradleProperty("production").map { it.toBoolean() }.orElse(false).get()
val ver = if (release) checked(
gitTag()?.removePrefix("jetbrains/")
?: error("Missing JetBrains plugin version. Publish builds must run from a jetbrains/<version> tag."),
) else checked(gitTag()?.removePrefix("jetbrains/") ?: "0.0.0-dev")
val notes = providers.gradleProperty("kilo.changeNotes").orElse("Release candidate build.")
val channel = providers.gradleProperty("kilo.channel").map { it.trim() }.orElse("default")
version = ver
plugins {
application
@@ -59,9 +84,39 @@ intellijPlatform {
splitMode = true
splitModeTarget = SplitModeTarget.BOTH
pluginConfiguration {
id = "ai.kilocode.jetbrains"
name = "Kilo Code"
version = provider { ver }
changeNotes = notes
ideaVersion {
untilBuild = provider { null }
}
vendor {
name = "Kilo Code"
url = "https://kilo.ai"
}
}
publishing {
token = providers.environmentVariable("JETBRAINS_MARKETPLACE_TOKEN")
channels = channel.map { value ->
if (value.isBlank() || value == "default") return@map listOf("default")
listOf(value)
}
}
signing {
certificateChain = providers.environmentVariable("JETBRAINS_CERTIFICATE_CHAIN")
privateKey = providers.environmentVariable("JETBRAINS_PRIVATE_KEY")
password = providers.environmentVariable("JETBRAINS_PRIVATE_KEY_PASSWORD")
}
pluginVerification {
ides {
create(IntelliJPlatformType.IntellijIdeaCommunity, libs.versions.intellij.platform)
create(IntelliJPlatformType.IntellijIdea, libs.versions.intellij.platform)
}
}
}