Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
388 changes: 205 additions & 183 deletions .github/workflows/build.yml

Large diffs are not rendered by default.

95 changes: 95 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Publishes the plugin to the JetBrains Marketplace.
#
# This runs ONLY after a maintainer manually publishes a GitHub Release (the draft created by
# build.yml). It is never triggered by a push or by draft creation, so Marketplace credentials
# are only read for a trusted, human-approved "release: published" event.

name: Release

on:
release:
types: [ published ]

# Avoid double-publishing if the event is delivered more than once for the same tag.
concurrency:
group: marketplace-publish-${{ github.event.release.tag_name }}
cancel-in-progress: false

permissions:
contents: read

jobs:
publish:
name: Publish to JetBrains Marketplace
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Fetch Sources (at the released tag)
uses: actions/checkout@v4
with:
ref: ${{ github.event.release.tag_name }}

- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: zulu
java-version: 17

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4

- name: Make gradlew executable
run: chmod +x gradlew

# The release tag must exactly match the project version that will actually be published.
- name: Verify release tag matches project version
shell: bash
run: |
set -euo pipefail
VERSION="$(./gradlew properties --console=plain -q --no-configuration-cache | grep "^version:" | cut -f2- -d ' ')"
if [ -z "${VERSION}" ]; then
echo "::error::Unable to read project version from Gradle properties"
exit 1
fi
TAG="${{ github.event.release.tag_name }}"
# Accept both the bare version and the legacy "v"-prefixed form (e.g. v2.0.0).
NORMALIZED_TAG="${TAG#v}"
echo "Project version: ${VERSION}"
echo "Release tag: ${TAG} (normalized: ${NORMALIZED_TAG})"
if [ "${NORMALIZED_TAG}" != "${VERSION}" ]; then
echo "::error::Release tag '${TAG}' does not match project version '${VERSION}'"
exit 1
fi

# Fail early (and clearly) when a publishing/signing secret is missing. Only the names are
# printed, never the values.
- name: Check required secrets
env:
PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }}
CERTIFICATE_CHAIN: ${{ secrets.CERTIFICATE_CHAIN }}
PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }}
PRIVATE_KEY_PASSWORD: ${{ secrets.PRIVATE_KEY_PASSWORD }}
shell: bash
run: |
set -euo pipefail
missing=()
[ -n "${PUBLISH_TOKEN:-}" ] || missing+=("PUBLISH_TOKEN")
[ -n "${CERTIFICATE_CHAIN:-}" ] || missing+=("CERTIFICATE_CHAIN")
[ -n "${PRIVATE_KEY:-}" ] || missing+=("PRIVATE_KEY")
[ -n "${PRIVATE_KEY_PASSWORD:-}" ] || missing+=("PRIVATE_KEY_PASSWORD")
if [ ${#missing[@]} -ne 0 ]; then
echo "::error::Missing required secrets: ${missing[*]}"
exit 1
fi
echo "All required secrets are present."

# publishPlugin depends on signPlugin (signing config) and patchChangelog, so the plugin is
# signed before it is uploaded. Credentials are read from the environment by build.gradle.kts.
- name: Publish plugin
env:
PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }}
CERTIFICATE_CHAIN: ${{ secrets.CERTIFICATE_CHAIN }}
PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }}
PRIVATE_KEY_PASSWORD: ${{ secrets.PRIVATE_KEY_PASSWORD }}
run: ./gradlew publishPlugin --console=plain
63 changes: 63 additions & 0 deletions .github/workflows/run-plugin-verifier.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Runs the IntelliJ Plugin Verifier on demand and on a weekly schedule.
#
# The Plugin Verifier downloads additional IDE distributions and is slow / disk-heavy, so it is
# kept out of the main Build workflow and never blocks release-draft creation. The Gradle
# `pluginVerification { ... }` configuration is retained so it can also be run locally.

name: Run Plugin Verifier

on:
workflow_dispatch:
schedule:
# Every Monday at 06:00 UTC.
- cron: '0 6 * * 1'

permissions:
contents: read

jobs:
verify:
name: Verify plugin
runs-on: ubuntu-latest
timeout-minutes: 120
permissions:
contents: read
steps:
- name: Maximize Build Space
uses: jlumbroso/free-disk-space@main
with:
tool-cache: false
large-packages: false

- name: Fetch Sources
uses: actions/checkout@v4

- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: zulu
java-version: 17

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4

- name: Make gradlew executable
run: chmod +x gradlew

- name: Setup Plugin Verifier IDEs Cache
uses: actions/cache@v4
with:
path: ~/.pluginVerifier/ides
key: plugin-verifier-${{ hashFiles('build/listProductsReleases.txt') }}

- name: Run Plugin Verification tasks
run: ./gradlew verifyPlugin -Dplugin.verifier.home.dir=~/.pluginVerifier --console=plain

- name: Collect Plugin Verifier Result
if: ${{ always() }}
uses: actions/upload-artifact@v4
with:
name: pluginVerifier-result
path: build/reports/pluginVerifier
retention-days: 7
if-no-files-found: ignore
70 changes: 35 additions & 35 deletions .github/workflows/run-ui-tests.yml
Original file line number Diff line number Diff line change
@@ -1,63 +1,63 @@
# GitHub Actions Workflow for launching UI tests on Linux, Windows, and Mac in the following steps:
# - Prepare and launch IDE with your plugin and robot-server plugin, which is needed to interact with the UI.
# - Wait for IDE to start.
# - Run UI tests with a separate Gradle task.
# GitHub Actions Workflow for running the headless Swing/UI component tests (the `ideaUiTest`
# Gradle task) on Linux, Windows, and macOS.
#
# Please check https://github.com/JetBrains/intellij-ui-test-robot for information about UI tests with IntelliJ Platform.
# These are plain headless component tests, so this workflow does NOT start a RemoteRobot server
# and does NOT launch a full IDE. If the project later gains real RemoteRobot client tests, add a
# separate workflow that runs runIdeForUiTests + robot-server and the RemoteRobot client.
#
# Workflow is triggered manually.
# Triggered manually.

name: Run UI Tests

on:
workflow_dispatch
workflow_dispatch:

permissions:
contents: read

jobs:

testUI:
ideaUiTest:
name: UI tests (${{ matrix.os }})
runs-on: ${{ matrix.os }}
timeout-minutes: 60
permissions:
contents: read
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
runIde: |
export DISPLAY=:99.0
Xvfb -ac :99 -screen 0 1920x1080x16 &
gradle runIdeForUiTests &
- os: windows-latest
runIde: start gradlew.bat runIdeForUiTests
- os: macos-latest
runIde: ./gradlew runIdeForUiTests &
os: [ ubuntu-latest, windows-latest, macos-latest ]

steps:

# Check out the current repository
- name: Fetch Sources
uses: actions/checkout@v4

# Set up Java environment for the next steps
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: zulu
java-version: 17

# Setup Gradle
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4

# Run IDEA prepared for UI testing
- name: Run IDE
run: ${{ matrix.runIde }}
- name: Make gradlew executable
shell: bash
run: chmod +x gradlew

# Wait for IDEA to be started
- name: Health Check
uses: jtalk/url-health-check-action@v4
with:
url: http://127.0.0.1:8082
max-attempts: 15
retry-delay: 30s
# Run via the Gradle wrapper through bash on every OS (Git Bash is available on Windows).
- name: Run headless Swing/UI component tests
shell: bash
run: ./gradlew ideaUiTest --no-configuration-cache --console=plain

# Run tests
- name: Tests
run: ./gradlew test
# Upload a separate, short-lived report per OS, only when the tests fail.
- name: Upload UI test report
if: ${{ failure() }}
uses: actions/upload-artifact@v4
with:
name: ui-test-report-${{ matrix.os }}
path: |
build/reports/tests
build/test-results
retention-days: 7
if-no-files-found: ignore
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
# runtime-pivot-plugin Changelog

## [Unreleased]
### Changed
- 重构 GitHub Actions CI/CD 流程:分层测试(单元 / 集成 / 无头 UI 组件)、推送主分支时自动创建并刷新 Release 草稿、GitHub Release 正式发布后自动签名并上传 JetBrains Marketplace
- Plugin Verifier 改为按需 / 每周定时单独运行,不再阻塞 Release 草稿创建

## [2.0.0] - 2025-03-23
### Refactor
Expand Down
40 changes: 40 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,46 @@ tasks {
}

intellijPlatformTesting {
// The CI test stage is split into three tiers backed by the single `test` source set
// (selected by package). They are routed through the IntelliJ Platform test runtime so
// that BasePlatformTestCase fixtures and Swing components work without a running IDE.
//
// IntelliJ Platform test fixtures are not compatible with the Gradle Configuration Cache,
// so each task is explicitly marked incompatible (the CI workflow also passes
// --no-configuration-cache) to avoid cache-serialization failures after tests pass.
testIde {
register("unitTest") {
task {
group = "verification"
description = "Runs fast unit tests that do not require an IntelliJ Platform fixture."
useJUnit()
filter { includeTestsMatching("com.runtime.pivot.plugin.unit.*") }
systemProperty("java.awt.headless", "true")
notCompatibleWithConfigurationCache("IntelliJ Platform test runtime is not configuration-cache compatible")
}
}
register("integrationTest") {
task {
group = "verification"
description = "Runs IntelliJ Platform integration tests (BasePlatformTestCase)."
useJUnit()
filter { includeTestsMatching("com.runtime.pivot.plugin.integration.*") }
systemProperty("java.awt.headless", "true")
notCompatibleWithConfigurationCache("IntelliJ Platform test runtime is not configuration-cache compatible")
}
}
register("ideaUiTest") {
task {
group = "verification"
description = "Runs headless Swing/UI component tests (no RemoteRobot server)."
useJUnit()
filter { includeTestsMatching("com.runtime.pivot.plugin.ui.*") }
systemProperty("java.awt.headless", "true")
notCompatibleWithConfigurationCache("IntelliJ Platform test runtime is not configuration-cache compatible")
}
}
}

runIde {
register("runIdeForUiTests") {
task {
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ pluginGroup = com.runtime.pivot.plugin
pluginName = runtime-pivot
pluginRepositoryUrl = https://github.com/wl2027/runtime-pivot
# SemVer format -> https://semver.org
pluginVersion = 2.0.0
pluginVersion = 2.1.0

# Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
pluginSinceBuild = 221
Expand Down
Empty file modified gradlew
100644 → 100755
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.runtime.pivot.plugin.integration;

import com.intellij.ide.plugins.PluginManagerCore;
import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.extensions.PluginId;
import com.intellij.testFramework.fixtures.BasePlatformTestCase;
import com.runtime.pivot.plugin.config.RuntimePivotConstants;
import com.runtime.pivot.plugin.config.RuntimePivotSettings;

/**
* IntelliJ Platform integration tests. These run inside a light platform fixture so that the
* plugin under development is loaded and its services / actions are registered.
*/
public class RuntimePivotPluginIntegrationTest extends BasePlatformTestCase {

public void testPluginIsLoaded() {
PluginId pluginId = PluginId.getId(RuntimePivotConstants.PLUGIN_ID);
assertNotNull("Plugin descriptor for " + RuntimePivotConstants.PLUGIN_ID + " should be available",
PluginManagerCore.getPlugin(pluginId));
}

public void testProjectServiceIsRegistered() {
RuntimePivotSettings settings = RuntimePivotSettings.getInstance(getProject());
assertNotNull("RuntimePivotSettings project service should be available", settings);
assertTrue("attachAgent should default to true", settings.isAttachAgent());
}

public void testActionsAreRegistered() {
ActionManager actionManager = ActionManager.getInstance();
assertNotNull("RuntimePivot.ClassLoaderTree action should be registered",
actionManager.getAction("RuntimePivot.ClassLoaderTree"));
assertNotNull("RuntimePivot.XSessionMonitoring action should be registered",
actionManager.getAction("RuntimePivot.XSessionMonitoring"));
}
}
Loading
Loading