diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4bf918597..87afae300 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -97,6 +97,12 @@ jobs: working-directory: /usr/share/dotnet run: dotnet new install Aspire.ProjectTemplates@13.6.4 + # Install Aspire CLI + - name: Install Aspire CLI + run: | + curl -fsSL https://aspire.dev/install.sh | bash -s -- --version 13.6.4 + echo "$HOME/.aspire/bin" >> "$GITHUB_PATH" + # Install MAUI workload - name: Install MAUI workload working-directory: /usr/share/dotnet diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ee04c1cf..f1d0b2ea7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,18 @@ ## [Unreleased] +### Added + +- Publish and deploy Aspire AppHosts actions + +### Changed + +- Update Rider SDK version to 2026.3-RC1 + +### Removed + +- Aspire manifest generation actions and service + ### Fixed - [RIDER-144901](https://youtrack.jetbrains.com/issue/RIDER-144901) Exit code is missing when stopping a service from the Services tool window diff --git a/README.md b/README.md index a76b220ef..b559c01ad 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,6 @@ The plugin provides support for Aspire projects in JetBrains Rider. **Features:** * Support for running and debugging of Aspire projects * Resource dashboard -* Manifest generation * Automatic database connection **Documentation**: diff --git a/build.gradle.kts b/build.gradle.kts index 3843fad10..e91a4b12e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -59,6 +59,8 @@ dependencies { testBundledModule("intellij.rd.client") testBundledPlugin("intellij.structureView.plugin") testBundledPlugin("intellij.bookmarks.plugin") + testBundledPlugin("org.jetbrains.plugins.terminal") + testBundledModule("intellij.terminal.frontend") } compileOnly(libs.serializationJson) @@ -83,6 +85,7 @@ intellijPlatform { } pluginVerification { + ignoredProblemsFile = layout.projectDirectory.file("gradle/plugin-verifier-ignored-problems.txt") failureLevel = listOf( VerifyPluginTask.FailureLevel.COMPATIBILITY_PROBLEMS, VerifyPluginTask.FailureLevel.OVERRIDE_ONLY_API_USAGES, diff --git a/core.frontend/build.gradle.kts b/core.frontend/build.gradle.kts index 471660223..3ae896a4c 100644 --- a/core.frontend/build.gradle.kts +++ b/core.frontend/build.gradle.kts @@ -14,6 +14,8 @@ dependencies { } bundledModule("intellij.platform.frontend") + bundledPlugin("org.jetbrains.plugins.terminal") + bundledModule("intellij.terminal.frontend") } implementation(project(":shared")) diff --git a/core.frontend/src/main/kotlin/com/jetbrains/aspire/actions/dashboard/host/AspireAppHostDeploymentAction.kt b/core.frontend/src/main/kotlin/com/jetbrains/aspire/actions/dashboard/host/AspireAppHostDeploymentAction.kt new file mode 100644 index 000000000..a3c885891 --- /dev/null +++ b/core.frontend/src/main/kotlin/com/jetbrains/aspire/actions/dashboard/host/AspireAppHostDeploymentAction.kt @@ -0,0 +1,26 @@ +package com.jetbrains.aspire.actions.dashboard.host + +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project +import com.jetbrains.aspire.deployment.AspireAppHostDeploymentService +import com.jetbrains.aspire.deployment.AspireDeploymentOperation +import com.jetbrains.aspire.deployment.AspireDeploymentTerminalRunner +import com.jetbrains.aspire.worker.AspireAppHostData +import kotlinx.coroutines.launch + +internal abstract class AspireAppHostDeploymentAction( + private val operation: AspireDeploymentOperation, +) : AspireAppHostBaseAction() { + override fun performAction(event: AnActionEvent, appHostData: AspireAppHostData, project: Project) { + event.coroutineScope.launch { + val commandLine = project.service() + .prepareCommand(appHostData.id, operation) ?: return@launch + project.service().run(operation, commandLine) + } + } + + override fun updateAction(event: AnActionEvent, appHostData: AspireAppHostData) { + event.presentation.isEnabledAndVisible = true + } +} diff --git a/core.frontend/src/main/kotlin/com/jetbrains/aspire/actions/dashboard/host/DeployAppHostAction.kt b/core.frontend/src/main/kotlin/com/jetbrains/aspire/actions/dashboard/host/DeployAppHostAction.kt new file mode 100644 index 000000000..2e5e93fe7 --- /dev/null +++ b/core.frontend/src/main/kotlin/com/jetbrains/aspire/actions/dashboard/host/DeployAppHostAction.kt @@ -0,0 +1,5 @@ +package com.jetbrains.aspire.actions.dashboard.host + +import com.jetbrains.aspire.deployment.AspireDeploymentOperation + +internal class DeployAppHostAction : AspireAppHostDeploymentAction(AspireDeploymentOperation.Deploy) diff --git a/core.frontend/src/main/kotlin/com/jetbrains/aspire/actions/dashboard/host/PublishAppHostAction.kt b/core.frontend/src/main/kotlin/com/jetbrains/aspire/actions/dashboard/host/PublishAppHostAction.kt new file mode 100644 index 000000000..2ddd02e41 --- /dev/null +++ b/core.frontend/src/main/kotlin/com/jetbrains/aspire/actions/dashboard/host/PublishAppHostAction.kt @@ -0,0 +1,5 @@ +package com.jetbrains.aspire.actions.dashboard.host + +import com.jetbrains.aspire.deployment.AspireDeploymentOperation + +internal class PublishAppHostAction : AspireAppHostDeploymentAction(AspireDeploymentOperation.Publish) diff --git a/core.frontend/src/main/kotlin/com/jetbrains/aspire/deployment/AspireDeploymentTerminalRunner.kt b/core.frontend/src/main/kotlin/com/jetbrains/aspire/deployment/AspireDeploymentTerminalRunner.kt new file mode 100644 index 000000000..61b83cf11 --- /dev/null +++ b/core.frontend/src/main/kotlin/com/jetbrains/aspire/deployment/AspireDeploymentTerminalRunner.kt @@ -0,0 +1,44 @@ +@file:Suppress("UnstableApiUsage") + +package com.jetbrains.aspire.deployment + +import com.intellij.openapi.application.EDT +import com.intellij.openapi.components.Service +import com.intellij.openapi.project.Project +import com.intellij.platform.eel.path.EelPath +import com.intellij.platform.eel.provider.asNioPath +import com.intellij.platform.eel.provider.getEelDescriptor +import com.intellij.terminal.frontend.toolwindow.TerminalToolWindowTabsManager +import com.jetbrains.aspire.AspireCoreFrontendBundle +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.jetbrains.plugins.terminal.startup.TerminalProcessType + +@Service(Service.Level.PROJECT) +internal class AspireDeploymentTerminalRunner(private val project: Project) { + suspend fun run( + operation: AspireDeploymentOperation, + commandLine: AspireDeploymentCommandLine, + ) = withContext(Dispatchers.EDT) { + // The terminal selects its execution environment from the frontend's NIO path. + val workingDirectory = EelPath.parse(commandLine.workingDirectory.value, project.getEelDescriptor()).asNioPath() + val tabName = when (operation) { + AspireDeploymentOperation.Publish -> + AspireCoreFrontendBundle.message("deployment.terminal.publish.title", commandLine.appHostName) + AspireDeploymentOperation.Deploy -> + AspireCoreFrontendBundle.message("deployment.terminal.deploy.title", commandLine.appHostName) + } + TerminalToolWindowTabsManager + .getInstance(project) + .createTabBuilder() + .shellCommand(commandLine.command) + .workingDirectory(workingDirectory.toString()) + .processType(TerminalProcessType.NON_SHELL) + .tabName(tabName) + .requestFocus(true) + .deferSessionStartUntilUiShown(false) + .closeOnProcessTermination(false) + .restoreOnProjectReopen(false) + .createTab() + } +} diff --git a/core.frontend/src/main/resources/intellij.aspire.core.frontend.xml b/core.frontend/src/main/resources/intellij.aspire.core.frontend.xml index 749acddb0..21ad84b4a 100644 --- a/core.frontend/src/main/resources/intellij.aspire.core.frontend.xml +++ b/core.frontend/src/main/resources/intellij.aspire.core.frontend.xml @@ -3,6 +3,8 @@ + + messages.AspireCoreFrontendBundle @@ -40,6 +42,16 @@ icon="AllIcons.Actions.Suspend"> + + + + + + @@ -51,6 +63,9 @@ + + + diff --git a/core.frontend/src/main/resources/messages/AspireCoreFrontendBundle.properties b/core.frontend/src/main/resources/messages/AspireCoreFrontendBundle.properties index 26264b45d..71ffd9d25 100644 --- a/core.frontend/src/main/resources/messages/AspireCoreFrontendBundle.properties +++ b/core.frontend/src/main/resources/messages/AspireCoreFrontendBundle.properties @@ -14,6 +14,12 @@ action.Aspire.Host.Debug.description=Debug Aspire AppHost action.Aspire.Host.Stop.text=Stop action.Aspire.Host.Stop.GoToAction.text=Stop Aspire AppHost action.Aspire.Host.Stop.description=Stop Aspire AppHost +action.Aspire.Host.Publish.text=Publish +action.Aspire.Host.Publish.GoToAction.text=Publish Aspire AppHost +action.Aspire.Host.Publish.description=Publish Aspire AppHost in a terminal +action.Aspire.Host.Deploy.text=Deploy +action.Aspire.Host.Deploy.GoToAction.text=Deploy Aspire AppHost +action.Aspire.Host.Deploy.description=Deploy Aspire AppHost in a terminal action.Aspire.Resource.Start.text=Start action.Aspire.Resource.Start.GoToAction.text=Start Aspire Resource action.Aspire.Resource.Start.description=Start Aspire resource @@ -55,3 +61,6 @@ configurable.Aspire.show.environment.variables=Show environment variables on the configurable.Aspire.open.console.view=Prefer to open a resource console view resource.choose.command.popup.header=Choose a Resource Command + +deployment.terminal.publish.title=Aspire Publish: {0} +deployment.terminal.deploy.title=Aspire Deploy: {0} diff --git a/core/src/main/kotlin/com/jetbrains/aspire/deployment/AspireAppHostDeploymentServiceImpl.kt b/core/src/main/kotlin/com/jetbrains/aspire/deployment/AspireAppHostDeploymentServiceImpl.kt new file mode 100644 index 000000000..5175c0dfc --- /dev/null +++ b/core/src/main/kotlin/com/jetbrains/aspire/deployment/AspireAppHostDeploymentServiceImpl.kt @@ -0,0 +1,75 @@ +@file:Suppress("UnstableApiUsage") + +package com.jetbrains.aspire.deployment + +import com.intellij.diagnostic.rethrowControlFlowException +import com.intellij.ide.BrowserUtil +import com.intellij.notification.Notification +import com.intellij.notification.NotificationAction +import com.intellij.notification.NotificationType +import com.intellij.openapi.diagnostic.logger +import com.intellij.openapi.project.Project +import com.intellij.platform.eel.provider.asEelPath +import com.jetbrains.aspire.AspireCoreBundle +import com.jetbrains.aspire.cli.AspireCliLocator +import com.jetbrains.aspire.worker.AspireAppHostId +import com.jetbrains.aspire.worker.AspirePath +import com.jetbrains.aspire.worker.AspireWorker +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlin.io.path.isRegularFile + +internal class AspireAppHostDeploymentServiceImpl(private val project: Project) : AspireAppHostDeploymentService { + companion object { + private val LOG = logger() + } + + override suspend fun prepareCommand( + appHostId: AspireAppHostId, + operation: AspireDeploymentOperation, + ): AspireDeploymentCommandLine? = withContext(Dispatchers.IO) { + try { + val appHost = AspireWorker.getInstance(project).getAppHostById(appHostId) + if (appHost == null || !appHost.appHostPath.isRegularFile()) { + Notification( + "Aspire", + AspireCoreBundle.message("notification.aspire.deployment.app.host.not.found.title"), + AspireCoreBundle.message("notification.aspire.deployment.app.host.not.found.content"), + NotificationType.WARNING, + ).notify(project) + return@withContext null + } + + val appHostPath = appHost.appHostPath.toAbsolutePath() + val appHostEelPath = appHostPath.asEelPath() + val cliPath = AspireCliLocator.getInstance(project).locate() + if (cliPath == null) { + Notification( + "Aspire", + AspireCoreBundle.message("notification.aspire.cli.not.installed.title"), + AspireCoreBundle.message("notification.aspire.cli.not.installed.content"), + NotificationType.WARNING, + ).addAction( + NotificationAction.createSimple(AspireCoreBundle.message("notification.aspire.cli.install.action")) { + BrowserUtil.browse("https://aspire.dev/get-started/install-cli/") + } + ).notify(project) + return@withContext null + } + + val command = listOf(cliPath.toString(), operation.commandName, "--apphost", appHostEelPath.toString()) + val workingDirectory = AspirePath(appHostPath.parent.asEelPath().toString()) + AspireDeploymentCommandLine(appHost.name, command, workingDirectory) + } catch (e: Exception) { + rethrowControlFlowException(e) + LOG.warn("Unable to prepare Aspire deployment command", e) + Notification( + "Aspire", + AspireCoreBundle.message("notification.aspire.deployment.prepare.failed.title"), + AspireCoreBundle.message("notification.aspire.deployment.prepare.failed.content", e.message ?: ""), + NotificationType.ERROR, + ).notify(project) + null + } + } +} diff --git a/core/src/main/resources/intellij.aspire.core.xml b/core/src/main/resources/intellij.aspire.core.xml index 9bc980832..cc9c01d10 100644 --- a/core/src/main/resources/intellij.aspire.core.xml +++ b/core/src/main/resources/intellij.aspire.core.xml @@ -50,6 +50,9 @@ + + https://github.com/JetBrains/gradle-intellij-plugin#plugin-verifier-dsl # See https://jb.gg/intellij-platform-builds-list for available build versions. -pluginVerificationIdeVersion = 2026.3-EAP8-SNAPSHOT +pluginVerificationIdeVersion = 2026.3-RC1-SNAPSHOT dotnetBuildConfiguration = Release diff --git a/gradle/plugin-verifier-ignored-problems.txt b/gradle/plugin-verifier-ignored-problems.txt new file mode 100644 index 000000000..2731e9db3 --- /dev/null +++ b/gradle/plugin-verifier-ignored-problems.txt @@ -0,0 +1,10 @@ +// Temporary workaround for Plugin Verifier 1.411 against Rider EAP8 (RD-263.7307.48) +// and RC1 (RD-263.7307.198). +// Aspire is bundled in Rider. When verifying its update, the verifier uses the bundled +// plugin's layout and resolves Terminal's main JAR without its frontend content module. +// intellij.terminal.frontend is declared in intellij.aspire.core.frontend.xml and its +// classes are present in the SDK. The same artifact with only its plugin ID changed +// to an unbundled ID verifies as Compatible, including all Terminal API calls. +// Remove this exception when the verifier handles updates of bundled plugins correctly. +// Scope it to 2.9.9 so the workaround must be reconsidered for the next plugin version. +me.rafaelldi.aspire:2.9.9:Package 'com\.intellij\.terminal\.frontend' is not found diff --git a/rider/src/main/kotlin/com/jetbrains/aspire/rider/actions/ManifestGenerationAction.kt b/rider/src/main/kotlin/com/jetbrains/aspire/rider/actions/ManifestGenerationAction.kt deleted file mode 100644 index 967121fe0..000000000 --- a/rider/src/main/kotlin/com/jetbrains/aspire/rider/actions/ManifestGenerationAction.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.jetbrains.aspire.rider.actions - -import com.intellij.openapi.actionSystem.ActionUpdateThread -import com.intellij.openapi.actionSystem.AnAction -import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.progress.currentThreadCoroutineScope -import com.intellij.platform.backend.workspace.virtualFile -import com.jetbrains.aspire.rider.manifest.ManifestService -import com.jetbrains.aspire.rider.util.isAspireHostProject -import com.jetbrains.rider.projectView.workspace.getProjectModelEntity -import com.jetbrains.rider.runtime.RiderDotNetActiveRuntimeHost -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch - -class ManifestGenerationAction : AnAction() { - override fun actionPerformed(event: AnActionEvent) { - val project = event.project ?: return - val entity = event.dataContext.getProjectModelEntity() ?: return - if (!entity.isAspireHostProject()) return - val file = entity.url?.virtualFile ?: return - - currentThreadCoroutineScope().launch(Dispatchers.Default) { - ManifestService.getInstance(project).generateManifest(file.toNioPath()) - } - } - - override fun update(event: AnActionEvent) { - val project = event.project - if (project == null) { - event.presentation.isEnabledAndVisible = false - return - } - - val runtime = RiderDotNetActiveRuntimeHost.getInstance(project).dotNetCoreRuntime.value - if (runtime == null) { - event.presentation.isEnabledAndVisible = false - return - } - - val entity = event.dataContext.getProjectModelEntity() - if (entity == null || !entity.isAspireHostProject()) { - event.presentation.isEnabledAndVisible = false - return - } - - event.presentation.isEnabledAndVisible = true - } - - override fun getActionUpdateThread() = ActionUpdateThread.BGT -} \ No newline at end of file diff --git a/rider/src/main/kotlin/com/jetbrains/aspire/rider/actions/dashboard/host/AspireManifestAction.kt b/rider/src/main/kotlin/com/jetbrains/aspire/rider/actions/dashboard/host/AspireManifestAction.kt deleted file mode 100644 index eb25f4cc3..000000000 --- a/rider/src/main/kotlin/com/jetbrains/aspire/rider/actions/dashboard/host/AspireManifestAction.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.jetbrains.aspire.rider.actions.dashboard.host - -import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.project.Project -import com.jetbrains.aspire.actions.dashboard.host.AspireAppHostBaseAction -import com.jetbrains.aspire.rider.manifest.ManifestService -import com.jetbrains.aspire.worker.AspireAppHostData -import com.jetbrains.aspire.worker.AspireWorker -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch - -class AspireManifestAction : AspireAppHostBaseAction() { - override fun performAction(event: AnActionEvent, appHostData: AspireAppHostData, project: Project) { - val mainFilePath = AspireWorker.getInstance(project).getAppHostById(appHostData.id)?.appHostPath ?: return - event.coroutineScope.launch(Dispatchers.Default) { - ManifestService.getInstance(project).generateManifest(mainFilePath) - } - } - - override fun updateAction(event: AnActionEvent, appHostData: AspireAppHostData) { - event.presentation.isEnabledAndVisible = true - } -} diff --git a/rider/src/main/kotlin/com/jetbrains/aspire/rider/manifest/ManifestService.kt b/rider/src/main/kotlin/com/jetbrains/aspire/rider/manifest/ManifestService.kt deleted file mode 100644 index b557b36bc..000000000 --- a/rider/src/main/kotlin/com/jetbrains/aspire/rider/manifest/ManifestService.kt +++ /dev/null @@ -1,100 +0,0 @@ -@file:Suppress("UnstableApiUsage") - -package com.jetbrains.aspire.rider.manifest - -import com.intellij.execution.util.ExecUtil -import com.intellij.notification.Notification -import com.intellij.notification.NotificationType -import com.intellij.openapi.application.EDT -import com.intellij.openapi.components.Service -import com.intellij.openapi.components.service -import com.intellij.openapi.diagnostic.debug -import com.intellij.openapi.diagnostic.logger -import com.intellij.openapi.fileEditor.FileEditorManager -import com.intellij.openapi.fileEditor.FileEditorOpenRequest -import com.intellij.openapi.project.Project -import com.intellij.openapi.vfs.LocalFileSystem -import com.intellij.platform.ide.progress.withBackgroundProgress -import com.jetbrains.aspire.AspireCoreBundle -import com.jetbrains.rider.runtime.RiderDotNetActiveRuntimeHost -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.jetbrains.annotations.ApiStatus -import java.nio.file.Path -import kotlin.io.path.absolutePathString - -/** - * A service responsible for the generation of an Aspire manifest file. - * - * This service handles the creation of an Aspire manifest file (`aspire-manifest.json`) for a given Aspire Host project. - */ -@ApiStatus.Internal -@Service(Service.Level.PROJECT) -class ManifestService(private val project: Project) { - companion object { - fun getInstance(project: Project) = project.service() - private val LOG = logger() - - private const val MANIFEST_FILE_NAME = "aspire-manifest.json" - } - - /** - * Generates an Aspire manifest file for the given Aspire host project. - * - * The method executes the `dotnet run --publisher manifest` command on a provided host project. - * - * @param hostProjectPath the path of the Aspire host project for which the manifest needs to be generated - */ - suspend fun generateManifest(hostProjectPath: Path) { - LOG.info("Generating Aspire manifest") - - withBackgroundProgress(project, AspireCoreBundle.message("progress.generating.aspire.manifest")) { - val runtime = RiderDotNetActiveRuntimeHost.getInstance(project).dotNetCoreRuntime.value - if (runtime == null) { - LOG.warn("Unable to find .NET runtime") - return@withBackgroundProgress - } - - val commandLine = runtime.createCommandLine( - listOf( - "run", - "--publisher", - "manifest", - "--output-path", - MANIFEST_FILE_NAME - ) - ) - val directoryPath = hostProjectPath.parent - commandLine.workDirectory = directoryPath.toFile() - LOG.debug { commandLine.commandLineString } - - val output = ExecUtil.execAndGetOutput(commandLine) - if (output.checkSuccess(LOG)) { - LOG.info("Aspire manifest is generated") - - val manifestPath = directoryPath.resolve(MANIFEST_FILE_NAME) - val file = LocalFileSystem.getInstance() - .refreshAndFindFileByPath(manifestPath.absolutePathString()) - ?.takeIf { it.isValid } - if (file != null) { - withContext(Dispatchers.EDT) { - val request = FileEditorOpenRequest.withFocus(true) - FileEditorManager - .getInstance(project) - .requestOpenFile(file, request) - } - } - } else { - withContext(Dispatchers.EDT) { - Notification( - "Aspire", - AspireCoreBundle.message("notification.manifest.unable.to.generate"), - output.stderr, - NotificationType.WARNING - ) - .notify(project) - } - } - } - } -} \ No newline at end of file diff --git a/rider/src/main/resources/intellij.aspire.rider.xml b/rider/src/main/resources/intellij.aspire.rider.xml index e77aee7d5..e8f145e64 100644 --- a/rider/src/main/resources/intellij.aspire.rider.xml +++ b/rider/src/main/resources/intellij.aspire.rider.xml @@ -85,18 +85,6 @@ icon="RiderIcons.Templates.TemplateAspire"> - - - - - - - - diff --git a/rider/src/main/resources/messages/AspireRiderBundle.properties b/rider/src/main/resources/messages/AspireRiderBundle.properties index 5767a5131..b62b89a6b 100644 --- a/rider/src/main/resources/messages/AspireRiderBundle.properties +++ b/rider/src/main/resources/messages/AspireRiderBundle.properties @@ -4,11 +4,6 @@ action.Aspire.AddToSolution.description=Add Aspire orchestration action.Aspire.AddToProject.text=Aspire Orchestration action.Aspire.AddToProject.GoToAction.text=Add Aspire Orchestration to the Project action.Aspire.AddToProject.description=Add Aspire orchestration -action.Aspire.Solution.Manifest.text=Generate Aspire Manifest -action.Aspire.Solution.Manifest.description=Generate Aspire manifest -action.Aspire.Host.Manifest.text=Generate Manifest -action.Aspire.Host.Manifest.GoToAction.text=Generate Aspire Manifest -action.Aspire.Host.Manifest.description=Generate Aspire manifest action.Aspire.Resource.Attach.text=Attach Debugger action.Aspire.Resource.Attach.GoToAction.text=Attach Debugger to Aspire Resource action.Aspire.Resource.Attach.description=Attach debugger to an Aspire resource diff --git a/shared/src/main/kotlin/com/jetbrains/aspire/deployment/AspireAppHostDeploymentService.kt b/shared/src/main/kotlin/com/jetbrains/aspire/deployment/AspireAppHostDeploymentService.kt new file mode 100644 index 000000000..dd6278439 --- /dev/null +++ b/shared/src/main/kotlin/com/jetbrains/aspire/deployment/AspireAppHostDeploymentService.kt @@ -0,0 +1,26 @@ +package com.jetbrains.aspire.deployment + +import com.jetbrains.aspire.worker.AspireAppHostId +import com.jetbrains.aspire.worker.AspirePath +import org.jetbrains.annotations.ApiStatus + +@ApiStatus.Internal +interface AspireAppHostDeploymentService { + suspend fun prepareCommand( + appHostId: AspireAppHostId, + operation: AspireDeploymentOperation, + ): AspireDeploymentCommandLine? +} + +@ApiStatus.Internal +enum class AspireDeploymentOperation(val commandName: String) { + Publish("publish"), + Deploy("deploy"), +} + +@ApiStatus.Internal +data class AspireDeploymentCommandLine( + val appHostName: String, + val command: List, + val workingDirectory: AspirePath, +) diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index d830913db..495284908 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -10,7 +10,6 @@
  • Support for running and debugging of Aspire projects
  • Unit testing
  • Resource dashboard
  • -
  • Manifest generation
  • Automatic database connection
  • ]]> diff --git a/src/test/kotlin/com/jetbrains/aspire/integration/DebuggingApplicationTests.kt b/src/test/kotlin/com/jetbrains/aspire/integration/DebuggingApplicationTests.kt index b041acdae..1b34ff68a 100644 --- a/src/test/kotlin/com/jetbrains/aspire/integration/DebuggingApplicationTests.kt +++ b/src/test/kotlin/com/jetbrains/aspire/integration/DebuggingApplicationTests.kt @@ -51,7 +51,7 @@ class DebuggingApplicationTests : DebuggerTestBase() { runTest( "DefaultAspireSolution.AppHost: http", fileForBreakpoint, - 12, + 14, "DefaultAspireSolution.AppHost: http", 1 ) { diff --git a/src/test/kotlin/com/jetbrains/aspire/integration/ManifestGenerationTests.kt b/src/test/kotlin/com/jetbrains/aspire/integration/ManifestGenerationTests.kt deleted file mode 100644 index bf9572d76..000000000 --- a/src/test/kotlin/com/jetbrains/aspire/integration/ManifestGenerationTests.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.jetbrains.aspire.integration - -import com.jetbrains.aspire.rider.manifest.ManifestService -import com.jetbrains.rider.projectView.solutionDirectoryPath -import com.jetbrains.rider.test.OpenSolutionParams -import com.jetbrains.rider.test.annotations.Solution -import com.jetbrains.rider.test.annotations.TestSettings -import com.jetbrains.rider.test.asserts.shouldBeTrue -import com.jetbrains.rider.test.enums.BuildTool -import com.jetbrains.rider.test.enums.sdk.SdkVersion -import com.jetbrains.rider.test.facades.solution.RiderSolutionApiFacade -import com.jetbrains.rider.test.facades.solution.SolutionApiFacade -import com.jetbrains.rider.test.framework.executeWithGold -import com.jetbrains.rider.test.junit5.base.PerClassSolutionTestBase -import com.jetbrains.rider.test.scriptingApi.runBlockingWithFlushing -import com.jetbrains.rider.test.shared.constants.TeamCityTags -import org.junit.jupiter.api.Tag -import org.junit.jupiter.api.Test -import kotlin.io.path.exists -import kotlin.io.path.readText -import kotlin.time.Duration.Companion.minutes - -@TestSettings(sdkVersion = SdkVersion.AUTODETECT, buildTool = BuildTool.AUTODETECT) -@Tag(TeamCityTags.General.Season) -@Solution("DefaultAspireSolution") -class ManifestGenerationTests : PerClassSolutionTestBase() { - override val solutionApiFacade: SolutionApiFacade = object : RiderSolutionApiFacade() { - override fun waitForSolution(params: OpenSolutionParams) { - // This may sometimes take a long time on CI agents. - params.projectModelReadyTimeout = params.projectModelReadyTimeout.multipliedBy(10L) - - return super.waitForSolution(params) - } - } - - @Test - fun `A manifest service should generate an Aspire manifest for a host project`() { - val service = ManifestService.getInstance(project) - val hostPath = project.solutionDirectoryPath - .resolve("DefaultAspireSolution.AppHost") - .resolve("DefaultAspireSolution.AppHost.csproj") - - runBlockingWithFlushing("Generating .NET Aspire manifest", 5.minutes) { - service.generateManifest(hostPath) - } - - val manifestFilePath = project.solutionDirectoryPath - .resolve("DefaultAspireSolution.AppHost") - .resolve("aspire-manifest.json") - - manifestFilePath.exists().shouldBeTrue() - - executeWithGold(testGoldFile) { printStream -> - printStream.print(manifestFilePath.readText()) - } - } -} diff --git a/src/test/kotlin/com/jetbrains/aspire/integration/PublishAppHostActionTest.kt b/src/test/kotlin/com/jetbrains/aspire/integration/PublishAppHostActionTest.kt new file mode 100644 index 000000000..07a2820a3 --- /dev/null +++ b/src/test/kotlin/com/jetbrains/aspire/integration/PublishAppHostActionTest.kt @@ -0,0 +1,111 @@ +@file:Suppress("UnstableApiUsage") + +package com.jetbrains.aspire.integration + +import com.intellij.openapi.actionSystem.ActionManager +import com.intellij.openapi.actionSystem.ActionPlaces +import com.intellij.openapi.actionSystem.ActionUiKind +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.actionSystem.ex.ActionUtil +import com.intellij.openapi.actionSystem.impl.SimpleDataContext +import com.intellij.openapi.application.EDT +import com.intellij.openapi.wm.ToolWindowManager +import com.intellij.terminal.frontend.toolwindow.TerminalToolWindowTabsManager +import com.intellij.terminal.frontend.view.TerminalViewSessionState +import com.jetbrains.aspire.actions.ASPIRE_APP_HOST_DATA +import com.jetbrains.aspire.worker.AspireWorker +import com.jetbrains.rider.test.OpenSolutionParams +import com.jetbrains.rider.test.annotations.Solution +import com.jetbrains.rider.test.annotations.TestSettings +import com.jetbrains.rider.test.enums.BuildTool +import com.jetbrains.rider.test.enums.sdk.SdkVersion +import com.jetbrains.rider.test.framework.executeWithGold +import com.jetbrains.rider.test.junit5.base.PerTestSolutionTestBase +import com.jetbrains.rider.test.scriptingApi.runBlockingWithFlushing +import com.jetbrains.rider.test.shared.constants.TeamCityTags +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.job +import kotlinx.coroutines.withContext +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import org.jetbrains.plugins.terminal.TerminalToolWindowFactory +import kotlin.io.path.exists +import kotlin.io.path.isRegularFile +import kotlin.io.path.readText +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.minutes + +@TestSettings(sdkVersion = SdkVersion.AUTODETECT, buildTool = BuildTool.AUTODETECT) +@Tag(TeamCityTags.General.Season) +internal class PublishAppHostActionTest : PerTestSolutionTestBase() { + override fun modifyOpenSolutionParams(params: OpenSolutionParams) { + super.modifyOpenSolutionParams(params) + params.restoreNuGetPackages = true + params.waitForCaches = true + } + + override val testLogManager by lazy { createAspireTestLogManager(traceScenarios, traceCategories) } + + @Test + @Solution("DefaultAspireSolution") + fun `publish generates expected Docker Compose`() { + val appHostDirectory = activeSolutionDirectory.resolve("DefaultAspireSolution.AppHost") + val appHostPath = appHostDirectory.resolve("DefaultAspireSolution.AppHost.csproj") + val composePath = appHostDirectory.resolve("aspire-output/docker-compose.yaml") + assertFalse(composePath.exists()) + + runBlockingWithFlushing("Publishing Aspire AppHost", 5.minutes) { + val worker = AspireWorker.getInstance(project) + val appHosts = worker.appHosts.first { hosts -> hosts.any { it.appHostPath == appHostPath } } + val appHost = appHosts.single { it.appHostPath == appHostPath } + val action = assertNotNull(ActionManager.getInstance().getAction("Aspire.Host.Publish")) + val dataContext = SimpleDataContext.builder() + .add(CommonDataKeys.PROJECT, project) + .add(ASPIRE_APP_HOST_DATA, appHost.data.value) + .build() + val event = + AnActionEvent.createEvent(action, dataContext, null, ActionPlaces.UNKNOWN, ActionUiKind.NONE, null) + val toolWindowManager = ToolWindowManager.getInstance(project) + val toolWindowId = TerminalToolWindowFactory.TOOL_WINDOW_ID + withContext(Dispatchers.EDT) { + if (toolWindowManager.getToolWindow(toolWindowId) == null) { + toolWindowManager.registerToolWindow(toolWindowId) {} + } + } + val tabsManager = TerminalToolWindowTabsManager.getInstance(project) + val initialTabs = withContext(Dispatchers.EDT) { tabsManager.tabs.toSet() } + + try { + withContext(Dispatchers.EDT) { + ActionUtil.performAction(action, event) + } + + var tab = withContext(Dispatchers.EDT) { tabsManager.tabs.firstOrNull { it !in initialTabs } } + while (tab == null) { + delay(100.milliseconds) + tab = withContext(Dispatchers.EDT) { tabsManager.tabs.firstOrNull { it !in initialTabs } } + } + tab.view.sessionState.first { it == TerminalViewSessionState.Terminated } + + assertTrue(composePath.isRegularFile(), "Aspire publish did not create $composePath") + executeWithGold(testGoldFile) { printStream -> + printStream.print(composePath.readText()) + } + } finally { + withContext(NonCancellable) { + val createdTabs = withContext(Dispatchers.EDT) { + tabsManager.tabs.filter { it !in initialTabs }.onEach(tabsManager::closeTab) + } + createdTabs.forEach { it.view.coroutineScope.coroutineContext.job.join() } + } + } + } + } +} diff --git a/testData/com/jetbrains/aspire/integration/DebuggingApplicationTests/Debugging default aspire solution pauses at host project/gold/Debugging default aspire solution pauses at host project.gold b/testData/com/jetbrains/aspire/integration/DebuggingApplicationTests/Debugging default aspire solution pauses at host project/gold/Debugging default aspire solution pauses at host project.gold index 20acb6a80..e17bd16e5 100644 --- a/testData/com/jetbrains/aspire/integration/DebuggingApplicationTests/Debugging default aspire solution pauses at host project/gold/Debugging default aspire solution pauses at host project.gold +++ b/testData/com/jetbrains/aspire/integration/DebuggingApplicationTests/Debugging default aspire solution pauses at host project/gold/Debugging default aspire solution pauses at host project.gold @@ -1,4 +1,4 @@ ---> AppHost.cs:12(387,409) // builder.Build().Run(); +--> AppHost.cs:14(436,458) // builder.Build().Run(); Program.Main() in DefaultAspireSolution.AppHost.dll +apiService = DistributedApplicationResourceBuilder args = string[0] diff --git a/testData/com/jetbrains/aspire/integration/ManifestGenerationTests/A manifest service should generate an Aspire manifest for a host project/gold/A manifest service should generate an Aspire manifest for a host project.gold b/testData/com/jetbrains/aspire/integration/ManifestGenerationTests/A manifest service should generate an Aspire manifest for a host project/gold/A manifest service should generate an Aspire manifest for a host project.gold deleted file mode 100644 index bd0d7f932..000000000 --- a/testData/com/jetbrains/aspire/integration/ManifestGenerationTests/A manifest service should generate an Aspire manifest for a host project/gold/A manifest service should generate an Aspire manifest for a host project.gold +++ /dev/null @@ -1,52 +0,0 @@ -{ - "resources": { - "apiservice": { - "type": "project.v0", - "path": "../DefaultAspireSolution.ApiService/DefaultAspireSolution.ApiService.csproj", - "env": { - "OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory", - "ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true", - "HTTP_PORTS": "{apiservice.bindings.http.targetPort}" - }, - "bindings": { - "http": { - "scheme": "http", - "protocol": "tcp", - "transport": "http" - }, - "https": { - "scheme": "https", - "protocol": "tcp", - "transport": "http" - } - } - }, - "webfrontend": { - "type": "project.v0", - "path": "../DefaultAspireSolution.Web/DefaultAspireSolution.Web.csproj", - "env": { - "OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory", - "ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true", - "HTTP_PORTS": "{webfrontend.bindings.http.targetPort}", - "APISERVICE_HTTP": "{apiservice.bindings.http.url}", - "services__apiservice__http__0": "{apiservice.bindings.http.url}", - "APISERVICE_HTTPS": "{apiservice.bindings.https.url}", - "services__apiservice__https__0": "{apiservice.bindings.https.url}" - }, - "bindings": { - "http": { - "scheme": "http", - "protocol": "tcp", - "transport": "http", - "external": true - }, - "https": { - "scheme": "https", - "protocol": "tcp", - "transport": "http", - "external": true - } - } - } - } -} \ No newline at end of file diff --git a/testData/com/jetbrains/aspire/integration/PublishAppHostActionTest/publish generates expected Docker Compose/gold/publish generates expected Docker Compose.gold b/testData/com/jetbrains/aspire/integration/PublishAppHostActionTest/publish generates expected Docker Compose/gold/publish generates expected Docker Compose.gold new file mode 100644 index 000000000..b325c2438 --- /dev/null +++ b/testData/com/jetbrains/aspire/integration/PublishAppHostActionTest/publish generates expected Docker Compose/gold/publish generates expected Docker Compose.gold @@ -0,0 +1,46 @@ +services: + compose-dashboard: + image: "mcr.microsoft.com/dotnet/nightly/aspire-dashboard:13.6" + ports: + - "18888" + expose: + - "18889" + - "18890" + networks: + - "aspire" + restart: "always" + apiservice: + image: "${APISERVICE_IMAGE}" + environment: + OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY: "in_memory" + ASPNETCORE_FORWARDEDHEADERS_ENABLED: "true" + HTTP_PORTS: "${APISERVICE_PORT}" + OTEL_EXPORTER_OTLP_ENDPOINT: "http://compose-dashboard:18889" + OTEL_EXPORTER_OTLP_PROTOCOL: "grpc" + OTEL_SERVICE_NAME: "apiservice" + expose: + - "${APISERVICE_PORT}" + networks: + - "aspire" + webfrontend: + image: "${WEBFRONTEND_IMAGE}" + environment: + OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY: "in_memory" + ASPNETCORE_FORWARDEDHEADERS_ENABLED: "true" + HTTP_PORTS: "${WEBFRONTEND_PORT}" + APISERVICE_HTTP: "http://apiservice:${APISERVICE_PORT}" + services__apiservice__http__0: "http://apiservice:${APISERVICE_PORT}" + APISERVICE_HTTPS: "https://apiservice:${APISERVICE_PORT}" + OTEL_EXPORTER_OTLP_ENDPOINT: "http://compose-dashboard:18889" + OTEL_EXPORTER_OTLP_PROTOCOL: "grpc" + OTEL_SERVICE_NAME: "webfrontend" + ports: + - "${WEBFRONTEND_PORT}" + depends_on: + apiservice: + condition: "service_started" + networks: + - "aspire" +networks: + aspire: + driver: "bridge" diff --git a/testData/solutions/DefaultAspireSolution/DefaultAspireSolution.AppHost/AppHost.cs b/testData/solutions/DefaultAspireSolution/DefaultAspireSolution.AppHost/AppHost.cs index df654a04e..4d52ac3ff 100644 --- a/testData/solutions/DefaultAspireSolution/DefaultAspireSolution.AppHost/AppHost.cs +++ b/testData/solutions/DefaultAspireSolution/DefaultAspireSolution.AppHost/AppHost.cs @@ -1,5 +1,7 @@ var builder = DistributedApplication.CreateBuilder(args); +builder.AddDockerComposeEnvironment("compose"); + var apiService = builder.AddProject("apiservice") .WithHttpHealthCheck("/health"); @@ -9,4 +11,4 @@ .WithReference(apiService) .WaitFor(apiService); -builder.Build().Run(); \ No newline at end of file +builder.Build().Run(); diff --git a/testData/solutions/DefaultAspireSolution/DefaultAspireSolution.AppHost/DefaultAspireSolution.AppHost.csproj b/testData/solutions/DefaultAspireSolution/DefaultAspireSolution.AppHost/DefaultAspireSolution.AppHost.csproj index da5a9160c..b385a766e 100644 --- a/testData/solutions/DefaultAspireSolution/DefaultAspireSolution.AppHost/DefaultAspireSolution.AppHost.csproj +++ b/testData/solutions/DefaultAspireSolution/DefaultAspireSolution.AppHost/DefaultAspireSolution.AppHost.csproj @@ -8,6 +8,10 @@ 0169f63d-5f25-4676-8ffe-ebee3ecb224b + + + +