diff --git a/.azure/pipelines/aspire-test.yml b/.azure/pipelines/aspire-test.yml index 4530f2b4..c43ac954 100644 --- a/.azure/pipelines/aspire-test.yml +++ b/.azure/pipelines/aspire-test.yml @@ -1,41 +1,70 @@ +parameters: +- name: linuxPool + displayName: 'Linux Pool' + type: object + default: + name: NetCore1ESPool-Internal + image: 1es-ubuntu-2204 + os: linux + stages: - stage: AspireTest + displayName: 'Aspire Regression' + # Disabled and not wired into any pipeline yet: enabling this in AzDO needs a + # review of pipeline policies (network isolation, feed access, container + # runtime availability on the pool). The GitHub Actions workflow + # (.github/workflows/aspire-regression.yml) provides this coverage in the + # meantime. To enable, remove this condition and add a template reference in + # pullrequest.yml and/or official.yml. condition: false - displayName: 'Aspire Tests' dependsOn: - Build - templateContext: - inputs: - - input: pipelineArtifact - artifactName: $(os)$(subos)_$(arch)_build - targetPath: $(Build.ArtifactStagingDirectory)/build - strategy: - matrix: - windows-amd64: - os: windows - arch: amd64 - extension: .exe - linux-amd64: - os: linux - arch: amd64 - extension: '' - macos-amd64: - os: darwin - arch: amd64 - extension: '' - steps: - - checkout: aspire - - task: UseDotNet@2 - displayName: 'Use .NET SDK' - inputs: - packageType: sdk - includePreviewVersions: true - useGlobalJson: true - workingDirectory: $(Build.SourcesDirectory)/aspire - - task: DotNetCoreCLI@2 - displayName: 'Run Aspire Tests' - inputs: - command: test - projects: '$(Build.SourcesDirectory)/aspire/tests/Aspire.Hosting.Tests/Aspire.Hosting.Tests.csproj' - env: - DcpPublisher__CliPath: $(Build.ArtifactStagingDirectory)/build/dcp$(extension) \ No newline at end of file + jobs: + - job: AspireRegressionLinux + displayName: 'Aspire Regression (latest daily)' + pool: ${{ parameters.linuxPool }} + templateContext: + inputs: + # Consume the Linux DCP binary produced by the Build stage. + - input: pipelineArtifact + artifactName: linux_amd64_build + targetPath: $(Pipeline.Workspace)/dcp + steps: + - checkout: self + + - task: UseDotNet@2 + displayName: 'Use .NET SDK' + inputs: + packageType: sdk + version: '10.0.x' + includePreviewVersions: true + + - task: bash@3 + displayName: 'Install latest daily Aspire CLI' + inputs: + targetType: inline + script: | + set -euo pipefail + curl -sSL https://aspire.dev/install.sh | bash -s -- -q dev --install-path "$HOME/.aspire/bin" + echo "##vso[task.prependpath]$HOME/.aspire/bin" + + - task: bash@3 + displayName: 'Run Aspire regression test' + inputs: + targetType: inline + workingDirectory: '$(Build.SourcesDirectory)' + script: | + set -euo pipefail + chmod +x "$(Pipeline.Workspace)/dcp/dcp" + test/aspire/run-regression.sh --dcp "$(Pipeline.Workspace)/dcp/dcp" + env: + DCP_DIAGNOSTICS_LOG_LEVEL: debug + DCP_DIAGNOSTICS_LOG_FOLDER: $(Build.ArtifactStagingDirectory)/aspire-test-logs + DCP_PRESERVE_EXECUTABLE_LOGS: 'true' + + - task: 1ES.PublishPipelineArtifact@1 + displayName: 'Publish Aspire Regression Logs' + condition: or(failed(), eq(variables['Agent.Diagnostic'], 'true')) + inputs: + targetPath: $(Build.ArtifactStagingDirectory)/aspire-test-logs + artifactName: aspire_test_logs diff --git a/.github/workflows/aspire-regression.yml b/.github/workflows/aspire-regression.yml new file mode 100644 index 00000000..4f8d0bdb --- /dev/null +++ b/.github/workflows/aspire-regression.yml @@ -0,0 +1,66 @@ +name: Aspire Regression + +# Runs a real Aspire AppHost, orchestrated by a freshly built DCP binary, against +# the latest Aspire daily build. This catches DCP regressions in the real Aspire +# integration (process orchestration, container creation, container networking) +# before a DCP build is inserted into Aspire. +# +# Runs on Linux only, where a Docker runtime is available on the hosted runner so +# the container resources in the AppHost can actually start. + +on: + workflow_dispatch: + schedule: + # Nightly, to catch drift against the moving Aspire daily build even when DCP + # itself has not changed. + - cron: '0 7 * * *' + push: + branches: + - main + pull_request: + branches: + - main + +permissions: + contents: read + +jobs: + aspire-regression: + name: Aspire regression (latest daily) + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup Go environment + uses: actions/setup-go@v5 + with: + go-version-file: 'go.mod' + architecture: 'x64' + + - name: Setup .NET SDK + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + + - name: Build DCP + run: make build + + - name: Install latest daily Aspire CLI + # The install script appends its install directory to $GITHUB_PATH when + # GITHUB_ACTIONS=true, so 'aspire' is available in later steps. + run: curl -sSL https://aspire.dev/install.sh | bash -s -- -q dev + + - name: Run Aspire regression test + run: test/aspire/run-regression.sh + env: + DCP_DIAGNOSTICS_LOG_LEVEL: debug + DCP_DIAGNOSTICS_LOG_FOLDER: ${{ github.workspace }}/test-logs + DCP_PRESERVE_EXECUTABLE_LOGS: 'true' + + - name: Upload DCP logs + uses: actions/upload-artifact@v4 + if: ${{ failure() }} + with: + name: aspire-regression-logs + path: ${{ github.workspace }}/test-logs/ diff --git a/test/aspire/apphost-body.cs b/test/aspire/apphost-body.cs new file mode 100644 index 00000000..fc57e562 --- /dev/null +++ b/test/aspire/apphost-body.cs @@ -0,0 +1,67 @@ +// Aspire end-to-end regression AppHost for DCP. +// +// This file is the body of a file-based (single-file) Aspire AppHost that is +// run against a locally built DCP binary to catch integration regressions before +// a DCP build is inserted into Aspire. It intentionally does not contain a +// "#:sdk" directive: run-regression.sh scaffolds a fresh AppHost with the +// latest Aspire daily CLI, preserves the generated daily SDK directive, appends +// this body, and runs it. +// +// The scenario includes both session and persistent containers. Resource names, +// container names, and container network aliases intentionally use mixed casing +// to exercise DCP's case handling (a past regression attached containers to the +// wrong network when network names were mixed case). + +using Aspire.Hosting.ApplicationModel; + +var runId = Environment.GetEnvironmentVariable("DCP_ASPIRE_REGRESSION_RUN_ID"); +if (string.IsNullOrWhiteSpace(runId)) +{ + runId = "manual"; +} + +var builder = DistributedApplication.CreateBuilder(new DistributedApplicationOptions +{ + Args = args, + DisableDashboard = true, +}); + +// Executable resource: exercises DCP process orchestration. +var worker = builder.AddExecutable("worker", "sleep", ".", "3600"); + +// Session container resource: exercises DCP container creation and cleanup. +var cache = builder.AddContainer("cache", "mcr.microsoft.com/cbl-mariner/busybox", "2.0") + .WithEntrypoint("sleep") + .WithArgs("3600") + .WithEnvironment("DCP_ASPIRE_REGRESSION_RUN_ID", runId) + .WithContainerNetworkAlias("Cache-Alias"); + +// Persistent containers: exercises Aspire's persistent container path and the +// DCP persistent network resources it creates for persistent containers. +var persistentCache = builder.AddContainer("persistent-cache", "mcr.microsoft.com/cbl-mariner/busybox", "2.0") + .WithEntrypoint("sleep") + .WithArgs("3600") + .WithEnvironment("DCP_ASPIRE_REGRESSION_RUN_ID", runId) + .WithLifetime(ContainerLifetime.Persistent) + .WithContainerName($"Persistent-Cache-MixedCase-{runId}") + .WithContainerNetworkAlias("Persistent-Cache-Alias"); + +var persistentWorker = builder.AddContainer("persistent-worker", "mcr.microsoft.com/cbl-mariner/busybox", "2.0") + .WithEntrypoint("sleep") + .WithArgs("3600") + .WithEnvironment("DCP_ASPIRE_REGRESSION_RUN_ID", runId) + .WithLifetime(ContainerLifetime.Persistent) + .WithContainerNetworkAlias("Persistent-Worker-Alias") + .WaitFor(persistentCache); + +// Session container depending on both session and persistent containers: +// exercises container network attachment and dependency ordering. +builder.AddContainer("web", "mcr.microsoft.com/cbl-mariner/busybox", "2.0") + .WithEntrypoint("sleep") + .WithArgs("3600") + .WithEnvironment("DCP_ASPIRE_REGRESSION_RUN_ID", runId) + .WithContainerNetworkAlias("Web-Alias") + .WaitFor(cache) + .WaitFor(persistentWorker); + +builder.Build().Run(); diff --git a/test/aspire/run-regression.sh b/test/aspire/run-regression.sh new file mode 100755 index 00000000..e84ea7fa --- /dev/null +++ b/test/aspire/run-regression.sh @@ -0,0 +1,137 @@ +#!/usr/bin/env bash +# +# Runs the Aspire end-to-end regression test against a locally built DCP binary. +# +# It scaffolds a minimal AppHost with the installed (ideally latest daily) Aspire +# CLI, keeps the scaffolded daily "#:sdk" directive, appends +# test/aspire/apphost-body.cs, and runs it while pointing Aspire at the local DCP +# build via DcpPublisher__CliPath. The test passes only if the Aspire CLI can +# start the AppHost and every expected resource becomes healthy. +# +# Prerequisites: +# - The Aspire CLI on PATH (install the daily channel with: +# curl -sSL https://aspire.dev/install.sh | bash -s -- -q dev ) +# - A .NET 10 SDK on PATH +# - A container runtime (Docker/Podman) available for the container resources +# +# Usage: +# test/aspire/run-regression.sh [--dcp /path/to/dcp] +# +# The DCP binary defaults to /bin/dcp (or bin/dcp.exe on Windows) and can +# be overridden with --dcp or the DCP_CLI_PATH environment variable. + +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "${script_dir}/../.." && pwd)" + +dcp_ext="" +case "$(uname -s 2>/dev/null || echo)" in + MINGW* | MSYS* | CYGWIN* | Windows_NT) dcp_ext=".exe" ;; +esac + +dcp_cli_path="${DCP_CLI_PATH:-${repo_root}/bin/dcp${dcp_ext}}" + +while [[ $# -gt 0 ]]; do + case "$1" in + --dcp) + dcp_cli_path="$2" + shift 2 + ;; + --dcp=*) + dcp_cli_path="${1#*=}" + shift + ;; + -h | --help) + sed -n '2,21p' "${BASH_SOURCE[0]}" + exit 0 + ;; + *) + echo "Unknown argument: $1" >&2 + exit 2 + ;; + esac +done + +if [[ ! -x "${dcp_cli_path}" ]]; then + echo "DCP binary not found or not executable at: ${dcp_cli_path}" >&2 + echo "Build it first (e.g. 'make build') or pass --dcp ." >&2 + exit 1 +fi + +if ! command -v aspire >/dev/null 2>&1; then + echo "The 'aspire' CLI was not found on PATH." >&2 + echo "Install the daily channel with:" >&2 + echo " curl -sSL https://aspire.dev/install.sh | bash -s -- -q dev" >&2 + exit 1 +fi + +work_dir="$(mktemp -d 2>/dev/null || mktemp -d -t dcp-aspire-regression)" +run_id="$(date -u +%Y%m%d%H%M%S)-$$" +apphost_started=false + +cleanup() { + if [[ "${apphost_started}" == "true" ]]; then + ( + cd "${work_dir}" + aspire stop --non-interactive --apphost apphost.cs >/dev/null 2>&1 || true + ) + fi + + rm -rf "${work_dir}" +} +trap cleanup EXIT + +echo "==> Scaffolding a minimal AppHost with the Aspire daily CLI" +aspire new aspire-empty \ + --name DcpRegression \ + --output "${work_dir}" \ + --channel daily \ + --language csharp \ + --non-interactive \ + --nologo \ + --suppress-agent-init + +generated_apphost="${work_dir}/apphost.cs" +if [[ ! -f "${generated_apphost}" ]]; then + echo "Scaffolding did not produce an apphost.cs in ${work_dir}" >&2 + exit 1 +fi + +# Preserve the daily SDK pin that the CLI scaffolded and append the committed +# AppHost body. The committed body intentionally has no "#:sdk" directive so the +# regression always runs against the latest daily Aspire AppHost SDK selected by +# the CLI scaffold. +sdk_line="$(grep -m1 '^#:sdk' "${generated_apphost}" || true)" +if [[ -z "${sdk_line}" ]]; then + echo "Could not find a '#:sdk' directive in the scaffolded apphost.cs" >&2 + exit 1 +fi + +echo "==> Using scaffolded SDK pin: ${sdk_line}" +{ + echo "${sdk_line}" + cat "${script_dir}/apphost-body.cs" +} >"${generated_apphost}" + +echo "==> Running the regression AppHost against DCP at: ${dcp_cli_path}" +output_log="${work_dir}/run.log" +( + cd "${work_dir}" + DcpPublisher__CliPath="${dcp_cli_path}" \ + DCP_ASPIRE_REGRESSION_RUN_ID="${run_id}" \ + ASPIRE_ALLOW_UNSECURED_TRANSPORT="true" \ + DOTNET_ENVIRONMENT="Development" \ + aspire run --detach --isolated --format Json --non-interactive --apphost apphost.cs +) 2>&1 | tee "${output_log}" +apphost_started=true + +for resource_name in worker cache persistent-cache persistent-worker web; do + echo "==> Waiting for Aspire resource to become healthy: ${resource_name}" + ( + cd "${work_dir}" + aspire wait "${resource_name}" --status healthy --timeout 300 --non-interactive --apphost apphost.cs + ) +done + +echo "==> Aspire regression test passed"