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
6 changes: 5 additions & 1 deletion .github/actions/setup-runtimes-caching/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ runs:
9.0.x
10.0.x

- name: Install Aspire CLI
uses: timheuer/setup-aspire@v0.1.0
with:
quality: release # temp workaround until url is fixed

- name: Set up Python
uses: actions/setup-python@v5
with:
Expand Down Expand Up @@ -129,4 +134,3 @@ runs:
if: ${{ inputs.name == 'Full' || contains(inputs.name, 'Hosting.Ollama') }}
with:
version: 0.11.8

2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,5 @@ nuget
target

node_modules
**/.modules/
**/*.AppHost.TypeScript/nuget.config
218 changes: 218 additions & 0 deletions eng/testing/validate-typescript-apphost.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
#!/usr/bin/env pwsh

param(
[Parameter(Mandatory = $true)]
[string]$AppHostPath,

[Parameter(Mandatory = $true)]
[string]$PackageProjectPath,

[Parameter(Mandatory = $true)]
[string]$PackageName,

[Parameter(Mandatory = $true)]
[string[]]$WaitForResources,

[string[]]$RequiredCommands = @(),

[string]$PackageVersion = "",

[ValidateSet("healthy", "up", "down")]
[string]$WaitStatus = "healthy",

[int]$WaitTimeoutSeconds = 180
)

$ErrorActionPreference = "Stop"

function Invoke-ExternalCommand {
param(
[Parameter(Mandatory = $true)]
[string]$FilePath,

[Parameter(Mandatory = $true)]
[string[]]$Arguments
)

& $FilePath @Arguments
if ($LASTEXITCODE -ne 0) {
$joinedArguments = [string]::Join(" ", $Arguments)
throw "Command failed with exit code ${LASTEXITCODE}: $FilePath $joinedArguments"
}
}

function Invoke-CleanupStep {
param(
[Parameter(Mandatory = $true)]
[string]$Description,

[Parameter(Mandatory = $true)]
[scriptblock]$Action,

[System.Collections.Generic.List[string]]$Failures = $null
)

try {
& $Action
}
catch {
$message = "Cleanup step '$Description' failed: $($_.Exception.Message)"
if ($null -ne $Failures) {
$Failures.Add($message)
return
}

throw $message
}
}

$resolvedAppHostPath = (Resolve-Path $AppHostPath).Path
$resolvedPackageProjectPath = (Resolve-Path $PackageProjectPath).Path
$appHostDirectory = Split-Path -Parent $resolvedAppHostPath
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\\..")).Path
$configPath = Join-Path $appHostDirectory "aspire.config.json"
$nugetConfigPath = Join-Path $appHostDirectory "nuget.config"
$localSource = Join-Path ([System.IO.Path]::GetTempPath()) ("ct-polyglot-" + [Guid]::NewGuid().ToString("N"))
$originalConfig = $null
$appStarted = $false
$primaryError = $null
$cleanupFailures = [System.Collections.Generic.List[string]]::new()

if ([string]::IsNullOrWhiteSpace($PackageVersion)) {
$versionPrefix = (& dotnet msbuild $resolvedPackageProjectPath -nologo -v:q -getProperty:VersionPrefix).Trim()
if ([string]::IsNullOrWhiteSpace($versionPrefix)) {
throw "Could not determine the evaluated VersionPrefix for $resolvedPackageProjectPath."
}

$PackageVersion = "$versionPrefix-polyglot.local"
}

if ($WaitForResources.Count -eq 1) {
$splitOptions = [System.StringSplitOptions]::RemoveEmptyEntries -bor [System.StringSplitOptions]::TrimEntries
$WaitForResources = $WaitForResources[0].Split(",", $splitOptions)
}

if ($RequiredCommands.Count -eq 1) {
$splitOptions = [System.StringSplitOptions]::RemoveEmptyEntries -bor [System.StringSplitOptions]::TrimEntries
$RequiredCommands = $RequiredCommands[0].Split(",", $splitOptions)
}

foreach ($commandName in $RequiredCommands) {
if ($null -eq (Get-Command $commandName -ErrorAction SilentlyContinue)) {
throw "Required command '$commandName' was not found on PATH."
}
}

try {
$originalConfig = Get-Content -Path $configPath -Raw
New-Item -ItemType Directory -Path $localSource -Force | Out-Null

Invoke-ExternalCommand "dotnet" @(
"pack",
$resolvedPackageProjectPath,
"-c", "Debug",
"-p:PackageVersion=$PackageVersion",
"-o", $localSource
)

$config = $originalConfig | ConvertFrom-Json -AsHashtable
if ($null -eq $config["packages"]) {
$config["packages"] = [ordered]@{}
}

$config["packages"][$PackageName] = $PackageVersion
$config | ConvertTo-Json -Depth 10 | Set-Content -Path $configPath -NoNewline

@"
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="local-polyglot" value="$localSource" />
</packageSources>
</configuration>
"@ | Set-Content -Path $nugetConfigPath -NoNewline

Push-Location $appHostDirectory
try {
Invoke-ExternalCommand "npm" @("ci")
Invoke-ExternalCommand "aspire" @(
"restore",
"--apphost", $resolvedAppHostPath,
"--non-interactive"
)
Invoke-ExternalCommand "npx" @("tsc", "--noEmit")
}
finally {
Pop-Location
}

Invoke-ExternalCommand "aspire" @(
"start",
"--apphost", $resolvedAppHostPath,
"--isolated",
"--format", "Json",
"--non-interactive"
)
$appStarted = $true

foreach ($resource in $WaitForResources) {
Invoke-ExternalCommand "aspire" @(
"wait",
$resource,
"--status", $WaitStatus,
"--apphost", $resolvedAppHostPath,
"--timeout", $WaitTimeoutSeconds
)
}

Invoke-ExternalCommand "aspire" @(
"describe",
"--apphost", $resolvedAppHostPath,
"--format", "Json"
)
}
catch {
$primaryError = $_
}
finally {
Invoke-CleanupStep -Description "restore Aspire config" -Action {
if ($null -ne $originalConfig) {
Set-Content -Path $configPath -Value $originalConfig -NoNewline
}
} -Failures $cleanupFailures

Invoke-CleanupStep -Description "remove generated nuget.config" -Action {
if (Test-Path $nugetConfigPath) {
Remove-Item $nugetConfigPath -Force
}
} -Failures $cleanupFailures

Invoke-CleanupStep -Description "remove local package source" -Action {
if (Test-Path $localSource) {
Remove-Item $localSource -Recurse -Force
}
} -Failures $cleanupFailures

Invoke-CleanupStep -Description "stop Aspire app" -Action {
if ($appStarted) {
Invoke-ExternalCommand "aspire" @(
"stop",
"--apphost", $resolvedAppHostPath
)
}
} -Failures $cleanupFailures
}

if ($cleanupFailures.Count -gt 0) {
$cleanupFailureMessage = "Cleanup failures:${Environment.NewLine}$($cleanupFailures -join [Environment]::NewLine)"
if ($null -ne $primaryError) {
Write-Error -Message $cleanupFailureMessage -ErrorAction Continue
}
else {
throw $cleanupFailureMessage
}
}

if ($null -ne $primaryError) {
throw $primaryError
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { mkdirSync, mkdtempSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { createBuilder } from './.modules/aspire.js';

const builder = await createBuilder();
const bindMountRoot = mkdtempSync(join(tmpdir(), 'activemq-polyglot-'));
const artemisDataPath = join(bindMountRoot, 'artemis-data');
const artemisConfPath = join(bindMountRoot, 'artemis-conf');

mkdirSync(artemisDataPath, { recursive: true });
mkdirSync(artemisConfPath, { recursive: true });

const mqPassword = await builder.addParameterWithValue("mq-password", "admin", {
secret: true
});
const mqUser = await builder.addParameterWithValue("mq-user", "admin", {
publishValueAsDefault: true
});

// addActiveMQ — ActiveMQ Classic with all parameters
const classic = await builder.addActiveMQ("classic", {
userName: mqUser,
password: mqPassword,
port: 36161,
scheme: "activemq",
webPort: 38161
});

// addActiveMQ — minimal overloads with explicit credentials for repeatable startup
const classic2 = await builder.addActiveMQ("classic2", {
userName: mqUser,
password: mqPassword
});

// addActiveMQArtemis — Artemis with all parameters
const artemis = await builder.addActiveMQArtemis("artemis", {
userName: mqUser,
password: mqPassword,
port: 36162,
scheme: "tcp",
webPort: 38162
});

// addActiveMQArtemis — minimal overloads with explicit credentials for repeatable startup
const artemis2 = await builder.addActiveMQArtemis("artemis2", {
userName: mqUser,
password: mqPassword
});

// withDataVolume — fluent chaining on Classic
await classic.withDataVolume({ name: "classic-data" });

// withConfVolume — fluent chaining on Classic
await classic.withConfVolume({ name: "classic-conf" });

// withDataBindMount — bind mount on Artemis
await artemis.withDataBindMount(artemisDataPath);

// withConfBindMount — bind mount on Artemis
await artemis.withConfBindMount(artemisConfPath);

// withDataVolume + withConfVolume — chaining on Artemis
await artemis2.withDataVolume();
await artemis2.withConfVolume();

// ---- Property access on ActiveMQServerResourceBase (ExposeProperties = true) ----
const classicResource = await classic;
const _classicEndpoint = await classicResource.primaryEndpoint.get();
const _classicHost = await classicResource.host.get();
const _classicPort = await classicResource.port.get();
const _classicUri = await classicResource.uriExpression.get();
const _classicCstr = await classicResource.connectionStringExpression.get();

const classic2Resource = await classic2;
const _classic2Host = await classic2Resource.host.get();
const _classic2Port = await classic2Resource.port.get();

const artemisResource = await artemis;
const _artemisEndpoint = await artemisResource.primaryEndpoint.get();
const _artemisHost = await artemisResource.host.get();
const _artemisPort = await artemisResource.port.get();
const _artemisUri = await artemisResource.uriExpression.get();
const _artemisCstr = await artemisResource.connectionStringExpression.get();

const artemis2Resource = await artemis2;
const _artemis2Host = await artemis2Resource.host.get();
const _artemis2Port = await artemis2Resource.port.get();

await builder.build().run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"appHost": {
"path": "apphost.ts",
"language": "typescript/nodejs"
},
"profiles": {
"https": {
"applicationUrl": "https://localhost:29750;http://localhost:28931",
"environmentVariables": {
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:10975",
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:13319"
}
}
},
"packages": {
"CommunityToolkit.Aspire.Hosting.ActiveMQ": ""
}
}
Loading
Loading