Publish IntelliJ‑platform plugins to a custom plugin repository (Artifactory, MinIO/S3, etc.) with a simple web UI
or a one‑liner Gradle task.
No manual editing of updatePlugins.xml required — it is generated/updated for you.
Custom plugin repositories are an official IntelliJ mechanism. See JetBrains docs:
https://plugins.jetbrains.com/docs/intellij/custom-plugin-repository.html
Uploader engine: https://github.com/brian-mcnamara/plugin_uploader
- Two ways to publish
- Web UI (Bridge mode): drag‑and‑drop
.jar/.zip, preview metadata, progress, logs. - Headless/CLI: run
gradle uploadPlugin -Pfile=...inside the container.
- Web UI (Bridge mode): drag‑and‑drop
- Works with many backends via HTTP PUT or S3 APIs: Artifactory, MinIO/S3, etc.
- Auto‑maintained
updatePlugins.xml— the feed the IDE consumes. - Preflight overwrite protection and readable diagnostics.
- i18n UI (English/中文).
- Docker or Docker Compose
- A custom plugin repository location you control (e.g., an Artifactory path or S3/MinIO bucket exposed via HTTP/S3 API)
- A token or basic credentials with write access to that location
Run the Bridge service locally and open the UI:
docker run --rm --name jetbrains-plugin-publisher \
-p 9876:9876 \
-e ARTIFACTORY_TOKEN=*********** \
-e PUBLISHER_BASE_URL='https://artifactory.example.com/artifactory/jetbrains-plugin-local' \
-e PUBLISHER_DOWNLOAD_PREFIX='https://artifactory.example.com/artifactory/jetbrains-plugin-local' \
-e PUBLISHER_REPO='artifactory' \
-e PUBLISHER_XML_NAME='updatePlugins.xml' \
-v "$PWD:/work" \
xooooooooox/jetbrains-plugin-publisherOR
docker run --rm --name jetbrains-plugin-publisher \
-p 9876:9876 \
-v "$PWD/gradle.properties:/app/gradle.properties:ro" \
-v "$PWD:/work" \
xooooooooox/jetbrains-plugin-publisherOpen http://127.0.0.1:9876/ in your browser.
services:
jpp:
container_name: jetbrains-plugin-publisher
image: xooooooooox/jetbrains-plugin-publisher
ports: [ "9876:9876" ]
environment:
# Optional auth for uploads; provide one of the following:
ARTIFACTORY_TOKEN: ${ARTIFACTORY_TOKEN:-} # Bearer token
PUBLISHER_BASIC: ${PUBLISHER_BASIC:-} # user:pass (rare)
# Repository targets
PUBLISHER_BASE_URL: ${PUBLISHER_BASE_URL:-https://artifactory-oss.example.com/artifactory/jetbrains-plugin-local}
PUBLISHER_DOWNLOAD_PREFIX: ${PUBLISHER_DOWNLOAD_PREFIX:-https://artifactory-oss.example.com/artifactory/jetbrains-plugin-local}
PUBLISHER_REPO: ${PUBLISHER_REPO:-artifactory}
PUBLISHER_XML_NAME: ${PUBLISHER_XML_NAME:-updatePlugins.xml}
volumes:
- $PWD:/work
restart: unless-stoppedOR
services:
jpp:
container_name: jetbrains-plugin-publisher
image: xooooooooox/jetbrains-plugin-publisher
ports: [ "9876:9876" ]
volumes:
- $PWD/gradle.properties:/app/gradle.properties
- $PWD:/work
restart: unless-stoppedWhy
PUBLISHER_DOWNLOAD_PREFIX?
This is the URL prefix your IDE will download from, typically the same path where binaries are uploaded.
These templates help you configure uploads either via Docker Compose (using environment variables) or via Gradle properties.
Loaded automatically by
docker composein this directory. If bothARTIFACTORY_TOKENandPUBLISHER_BASICare set, Bearer is typically used.
# Host-side BuildKit (recommended)
DOCKER_BUILDKIT=1
PUBLISHER_BASE_URL='https://artifactory.example.com/artifactory/jetbrains-plugins-local'
PUBLISHER_DOWNLOAD_PREFIX='https://artifactory.example.com/artifactory/jetbrains-plugins-local'
PUBLISHER_REPO=artifactory
PUBLISHER_XML_NAME=updatePlugins.xml
# Authentication for uploads (choose one)
# - Bearer token (preferred):
ARTIFACTORY_TOKEN=********
# - Basic auth in the form user:password
PUBLISHER_BASIC=Drop this next to your
build.gradle/settings.gradleto configure the Gradle uploader plugin.
publisher.repo=artifactory
publisher.baseUrl=https://artifactory.example.com/artifactory/jetbrains-plugin-local
publisher.downloadUrlPrefix=https://artifactory.example.com/artifactory/jetbrains-plugin-local
publisher.xmlName=updatePlugins.xml
# Authentication (pick one)
# 1) used as Bearer token(Access Token)
# publisher.token=**************
# 2) Basic auth (rarely needed)
# publisher.basic=myuser:my_pass- Open the page (
http://127.0.0.1:9876/). - Leave Mode = Bridge. The Bridge service reads defaults from container env vars.
- (Optional) Check Custom to override repository/auth at runtime.
- Click Choose Files (supports selecting a folder). The page parses each plugin’s
plugin.xmlto prefill ID/version/builds. - Click ▶ to upload. The page shows per‑file progress and server logs.
- On success, the container updates/creates
updatePlugins.xmlfor the feed.
You can upload without the UI, using the Gradle task backed by the plugin_uploader engine.
# Enter the running container (or run these in a dev shell):
docker exec -it jetbrains-plugin-publisher bash
# Upload a single plugin archive (.jar or .zip)
# Option1:
gradle -q uploadPlugin \
-Pfile=/work/your-plugin-1.2.3.jar \
# Option2:
gradle -q uploadPlugin \
-Pfile=/work/your-plugin-1.2.3.jar \
-PpluginId=com.yourco.yourplugin \
-PpluginVersion=1.2.3 \
-PsinceBuild=241 \
-PuntilBuild=251.* \
-PpluginName=com.yourco.yourplugin \
-PbaseUrl="$PUBLISHER_BASE_URL" \
-PdownloadUrlPrefix="$PUBLISHER_DOWNLOAD_PREFIX" \
-PxmlName="${PUBLISHER_XML_NAME:-updatePlugins.xml}"Authentication is resolved as follows (highest priority first):
- CLI flags
-Ptoken(Bearer) /-Pbasic(user:pass) - Environment:
ARTIFACTORY_TOKEN(orPUBLISHER_TOKEN) /PUBLISHER_BASIC gradle.properties~/.config/jpp/jpp.properties
| Purpose | Gradle property | Env var(s) | Notes |
|---|---|---|---|
| Repository kind | repo / publisher.repo |
PUBLISHER_REPO |
artifactory (REST PUT), s3, minio |
| Base URL (upload) | baseUrl / publisher.baseUrl |
PUBLISHER_BASE_URL |
Target root for binaries and updatePlugins.xml |
| Download URL prefix | downloadUrlPrefix / publisher.downloadUrlPrefix |
PUBLISHER_DOWNLOAD_PREFIX |
What IDE sees in the feed |
| Feed file | xmlName / publisher.xmlName |
PUBLISHER_XML_NAME |
Defaults to updatePlugins.xml |
| Token (Bearer) | token / publisher.token |
ARTIFACTORY_TOKEN, PUBLISHER_TOKEN |
Preferred |
| Basic auth | basic / publisher.basic |
PUBLISHER_BASIC |
Format user:pass (rare) |
| S3/MinIO key | auth / publisher.minioAuth |
MINIO_AUTH, AWS_ACCESS_KEY_ID |
For S3/MinIO mode |
Build range defaults: sinceBuild defaults to 241 (can override), untilBuild optional.
Overwrite safety: the Bridge server performs a HEAD/Range preflight; if a file exists and Protect is on, the
upload is skipped with 409 status.
IntelliJ/IDEA can install plugins from any HTTP location as long as there is a updatePlugins.xml feed that lists
plugins and versions.
This project automates both steps:
- Uploads your plugin archive to your storage (Artifactory/S3/etc.).
- Ensures
updatePlugins.xmlcontains/upserts an entry with:id,versionurl(built from Download URL Prefix + path/to/file)idea-version(since-build/until-build)- Optional
name,description,change-notes(parsed from plugin archive).
Point your IDE to the feed URL (Settings → Plugins → ⚙ → Manage Plugin Repositories).
Tips: Format of updatePlugins.xml
- 401/403: token/basic credentials missing or wrong for the target path.
- 404: base path does not exist in your repository.
plugin.xmlerror: the archive doesn’t containMETA-INF/plugin.xml.- Version unspecified: pass
-PpluginVersion=or fixplugin.xml’s<version>. - CORS errors (Direct mode): prefer Bridge mode unless your server allows PUT with
Authorizationfrom the browser.
Logs in the UI include timestamps; server returns masked commands for safety.
- JetBrains docs – Custom plugin repository: https://plugins.jetbrains.com/docs/intellij/custom-plugin-repository.html
- Gradle plugin uploader (engine): https://github.com/brian-mcnamara/plugin_uploader
This project is licensed under the MIT License. See LICENSE.
