diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index b3f2148..0000000 Binary files a/.DS_Store and /dev/null differ diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..ff152d0 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,101 @@ +# GitHub Actions Workflow is created for testing and preparing the plugin release draft. + +name: Build +on: + # Trigger the workflow on pushes to only the 'main' branch (this avoids duplicate checks being run e.g., for dependabot pull requests) + push: + branches: [ main ] + # Trigger the workflow on any pull request + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + + # Prepare environment and build the plugin + build: + name: Build + runs-on: ubuntu-latest + outputs: + version: ${{ steps.properties.outputs.version }} + steps: + + # Check out the current repository + - name: Fetch Sources + uses: actions/checkout@v6 + + # Set up Java environment for the next steps + - name: Setup Java + uses: actions/setup-java@v5 + with: + distribution: zulu + java-version: 21 + + # Setup Gradle + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v6 + + # Set environment variables + - name: Export Properties + id: properties + shell: bash + run: | + VERSION="$(grep '^version = ' plugin/build.gradle.kts | cut -d'"' -f2)" + echo "version=$VERSION" >> $GITHUB_OUTPUT + + # Build plugin + - name: Build plugin + run: ./gradlew packagePlugin + + # Prepare plugin archive content for creating artifact + - name: Prepare Plugin Artifact + id: artifact + shell: bash + run: | + cd ${{ github.workspace }}/plugin/build/distributions + FILENAME=`ls *.zip` + unzip "$FILENAME" -d content + + echo "filename=${FILENAME:0:-4}" >> $GITHUB_OUTPUT + + # Store already-built plugin as an artifact for downloading + - name: Upload artifact + uses: actions/upload-artifact@v7 + with: + name: ${{ steps.artifact.outputs.filename }} + path: ./plugin/build/distributions/content/*/* + + # Prepare a draft release for GitHub Releases page for the manual verification. + # If accepted and published, release workflow would be triggered. + releaseDraft: + name: Release draft + if: github.event_name != 'pull_request' + needs: [ build ] + runs-on: ubuntu-latest + permissions: + contents: write + steps: + + # Check out the current repository + - name: Fetch Sources + uses: actions/checkout@v6 + + # Remove old release drafts by using the curl request for the available releases with a draft flag + - name: Remove Old Release Drafts + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh api repos/{owner}/{repo}/releases \ + --jq '.[] | select(.draft == true) | .id' \ + | xargs -I '{}' gh api -X DELETE repos/{owner}/{repo}/releases/{} + + # Create a new release draft which is not publicly visible and requires manual acceptance + - name: Create Release Draft + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release create "v${{ needs.build.outputs.version }}" \ + --draft \ + --title "v${{ needs.build.outputs.version }}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..df402b9 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,46 @@ +# GitHub Actions Workflow created for handling the release process based on the draft release prepared with the Build workflow. + +name: Release +on: + release: + types: [prereleased, released] + +jobs: + + # Prepare and publish the plugin to JetBrains Marketplace + release: + name: Publish Plugin + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + + # Check out the current repository + - name: Fetch Sources + uses: actions/checkout@v6 + with: + ref: ${{ github.event.release.tag_name }} + + # Set up Java environment for the next steps + - name: Setup Java + uses: actions/setup-java@v5 + with: + distribution: zulu + java-version: 21 + + # Setup Gradle + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v6 + + # Publish the plugin to JetBrains Marketplace + - name: Publish Plugin + env: + JETBRAINS_MARKETPLACE_PUBLISH_TOKEN: ${{ secrets.JETBRAINS_MARKETPLACE_TOKEN }} + run: ./gradlew publishPlugin + + # Upload artifact as a release asset + - name: Upload Release Asset + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh release upload ${{ github.event.release.tag_name }} ./plugin/build/distributions/* diff --git a/README.md b/README.md index 08675ef..6a4fb7f 100644 --- a/README.md +++ b/README.md @@ -22,3 +22,11 @@ b. Once a workspace is up and running, follow the provided instructions to open c. Once the Toolbox App is connected to a remote, click it and choose an IDE to install in the CDE. d. Once a chosen IDE is installed, go to the previous page and click the project folder. Local ThinClient will connect to CDE. + + +## Release +- Find a draft release on the [Releases](https://github.com/redhat-developer/devspaces-toolbox-plugin/releases) page. The draft is created and updated automatically on each push to the `main` branch. +- Edit the draft: + - Click the `Generate release notes` button and edit the release notes if needed + - Click the `Publish release` button. The [Release](https://github.com/redhat-developer/devspaces-toolbox-plugin/blob/main/.github/workflows/release.yml) Workflow will attach the built plugin artifact to the published release and upload the plugin artifact to [JetBrains Marketplace](https://plugins.jetbrains.com/plugin/31372-red-hat-openshift-dev-spaces). +- Bump the `version` in the [gradle.properties](https://github.com/redhat-developer/devspaces-toolbox-plugin/blob/main/plugin/build.gradle.kts) file. diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts index f20c335..28e1f16 100644 --- a/build-logic/build.gradle.kts +++ b/build-logic/build.gradle.kts @@ -16,6 +16,7 @@ repositories { dependencies { implementation(libs.plugin.structure) implementation(libs.jackson.kotlin) + implementation(libs.marketplace.client) } gradlePlugin { @@ -32,6 +33,12 @@ gradlePlugin { displayName = "Install Toolbox Plugin" description = "Installs the plugin into the local Toolbox directory" } + create("toolboxPublish") { + id = "com.redhat.devtools.toolbox.publish" + implementationClass = "com.redhat.devtools.toolbox.buildlogic.PublishToolboxPlugin" + displayName = "Publish Toolbox Plugin" + description = "Packages and publishes a JetBrains Toolbox plugin to the JetBrains Marketplace" + } } } diff --git a/build-logic/src/main/kotlin/toolbox/buildlogic/PublishToolboxPlugin.kt b/build-logic/src/main/kotlin/toolbox/buildlogic/PublishToolboxPlugin.kt new file mode 100644 index 0000000..95a4c57 --- /dev/null +++ b/build-logic/src/main/kotlin/toolbox/buildlogic/PublishToolboxPlugin.kt @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2026 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package com.redhat.devtools.toolbox.buildlogic + +import org.gradle.api.DefaultTask +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.bundling.Zip +import org.gradle.work.DisableCachingByDefault +import org.jetbrains.intellij.pluginRepository.PluginRepositoryFactory +import org.jetbrains.intellij.pluginRepository.model.LicenseUrl +import org.jetbrains.intellij.pluginRepository.model.ProductFamily + +/** + * Gradle plugin that packages and publishes a JetBrains Toolbox plugin to the + * [JetBrains Marketplace](https://plugins.jetbrains.com). + */ +class PublishToolboxPlugin : Plugin { + override fun apply(target: Project) { + val packageTask = target.tasks.register("packagePlugin", Zip::class.java) { + dependsOn(target.tasks.named("assemble")) + from(target.layout.buildDirectory.file("generated/extension.json")) + from(target.file("src/main/resources")) { + include("dependencies.json") + include("icon.svg") + into("${target.group}") + } + from(target.tasks.named("jar")) { + into("${target.group}/lib") + } + } + + target.tasks.register("publishPlugin", PublishTask::class.java) { + dependsOn(packageTask) + extensionId.set(target.group.toString()) + pluginZipFile.set(packageTask.flatMap { it.archiveFile }) + vendor.set(target.extensions.extraProperties["vendor"].toString()) + } + } + + /** + * Task that uploads the packaged plugin ZIP to the JetBrains Marketplace. + * + * Requires the `JETBRAINS_MARKETPLACE_PUBLISH_TOKEN` environment variable to be set. + * The token can be generated from your JetBrains Marketplace account. + */ + @DisableCachingByDefault(because = "Publishes plugin to JetBrains Marketplace") + abstract class PublishTask : DefaultTask() { + @get:Input + abstract val extensionId: Property + + /** Path to the plugin ZIP archive produced by the `packagePlugin` task. */ + @get:InputFile + abstract val pluginZipFile: RegularFileProperty + + @get:Input + abstract val vendor: Property + + @TaskAction + fun publish() { + val jbMarketplaceToken: String? = System.getenv("JETBRAINS_MARKETPLACE_PUBLISH_TOKEN") + if (jbMarketplaceToken.isNullOrBlank()) { + error( + "Environment variable `JETBRAINS_MARKETPLACE_PUBLISH_TOKEN` is not set. " + "Please obtain a token from https://plugins.jetbrains.com and set it." + ) + } + + println("Publishing plugin ${extensionId.get()} to JetBrains Marketplace...") + + val instance = PluginRepositoryFactory.create( + "https://plugins.jetbrains.com", jbMarketplaceToken + ) + + val existingPlugin = instance.pluginManager.getPluginByXmlId( + extensionId.get(), ProductFamily.TOOLBOX + ) + + if (existingPlugin != null) { + instance.uploader.uploadUpdateByXmlIdAndFamily( + extensionId.get(), + ProductFamily.TOOLBOX, + pluginZipFile.get().asFile, + null, // channel – not yet supported for Toolbox plugins. + "Bug fixes and improvements", + false + ) + } else { + println("Plugin not found on Marketplace. Uploading as new plugin...") + instance.uploader.uploadNewPlugin( + pluginZipFile.get().asFile, + listOf("toolbox", "gateway"), // do not change + LicenseUrl.ECLIPSE_PUBLIC, + ProductFamily.TOOLBOX, + vendor = vendor.get(), + isHidden = true, + ) + } + println("Plugin published successfully!") + } + } +} diff --git a/build-logic/src/main/kotlin/toolbox/buildlogic/ToolboxGenerateJsonExtension.kt b/build-logic/src/main/kotlin/toolbox/buildlogic/ToolboxGenerateJsonExtension.kt index b30da9f..d46802c 100644 --- a/build-logic/src/main/kotlin/toolbox/buildlogic/ToolboxGenerateJsonExtension.kt +++ b/build-logic/src/main/kotlin/toolbox/buildlogic/ToolboxGenerateJsonExtension.kt @@ -84,7 +84,7 @@ class ToolboxGenerateJsonExtension : Plugin { extensionVersion.set(target.version.toString()) metaName.set("Red Hat OpenShift Dev Spaces") metaDescription.set("Red Hat OpenShift Dev Spaces Plugin for JetBrains Toolbox") - metaVendor.set("Red-Hat") + metaVendor.set(target.extensions.extraProperties["vendor"].toString()) metaUrl.set("https://www.redhat.com") destinationFile.set(extensionJsonFile) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7d5742f..19862ab 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,12 +2,14 @@ coroutines = "1.10.1" jackson = "2.18.2" kotlin = "2.1.0" +marketplace-client = "2.0.50" toolbox-plugin-api = "1.8.65679" plugin-structure = "3.320" [libraries] coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } jackson-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson" } +marketplace-client = { module = "org.jetbrains.intellij:plugin-repository-rest-client", version.ref = "marketplace-client" } toolbox-core-api = { module = "com.jetbrains.toolbox:core-api", version.ref = "toolbox-plugin-api" } toolbox-remote-dev-api = { module = "com.jetbrains.toolbox:remote-dev-api", version.ref = "toolbox-plugin-api" } toolbox-ui-api = { module = "com.jetbrains.toolbox:ui-api", version.ref = "toolbox-plugin-api" } diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts index e3307af..53b078a 100644 --- a/plugin/build.gradle.kts +++ b/plugin/build.gradle.kts @@ -3,6 +3,7 @@ plugins { `kotlin-dsl` id("com.redhat.devtools.toolbox.packaging") id("com.redhat.devtools.toolbox.install") + id("com.redhat.devtools.toolbox.publish") `java-library` } @@ -11,6 +12,8 @@ plugins { group = "com.redhat.devtools.toolbox" version = "0.0.1" +extra["vendor"] = "Red-Hat" + kotlin { jvmToolchain(21) }