Skip to content
Open
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
11 changes: 6 additions & 5 deletions docs/ci/test-trigger-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,12 +187,13 @@ Highlights:
`src/Aspire.Hosting.Integration.Analyzers/**` →
`test:Aspire.Hosting.Analyzers.Tests`.
- **non-.NET job loose triggers** — only the paths the project graph cannot
attribute, such as `tests/PolyglotAppHosts/**`, checked-in `*.ats.txt` /
attribute, such as `tests/PolyglotAppHosts/**`, checked-in
`*.tscompat.suppression.txt` baselines, `tools/TypeScriptApiCompat/**`, and
`extension/**`. A `src/Aspire.Hosting*/api/*.ats.txt` baseline fans out to
**both** `job:typescript-api-compat` (baseline diff) and `job:polyglot`,
because the polyglot playground regenerates and compiles that exported surface
in every language.
`extension/**`. Checked-in `*.ats.txt` baselines under the project `api/`
directories are
generated artifacts dropped by the prefilter (see below) when they are the
only changed files, so no dedicated rule routes them here. Hosting integration
projects with polyglot fixtures are listed in `affected_project_rules`.
- **loose-file deps** — `eng/clipack/**`, `eng/dashboardpack/**`,
`eng/dcppack/**`, `eng/winget/**`, `eng/homebrew/**`,
`src/Aspire.ProjectTemplates/**`, `playground/**`, `.github/workflows/**`,
Expand Down
7 changes: 7 additions & 0 deletions eng/github-ci/ci-skip-entirely-patterns.txt
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ eng/github-ci/ci-skip-entirely-patterns.txt
# Documentation
**.md

# Generated API and ATS baselines
# These files are generated release/API-surface artifacts and do not require PR CI.
src/*/api/*.cs
src/*/api/*.ats.txt
src/Components/*/api/*.cs
src/Components/*/api/*.ats.txt

# Engineering pipeline scripts (Azure DevOps, not used in the GitHub CI build)
eng/pipelines/**
eng/test-configuration.json
Expand Down
70 changes: 58 additions & 12 deletions eng/github-ci/test-trigger-map.yml
Original file line number Diff line number Diff line change
Expand Up @@ -199,17 +199,12 @@ path_rules:
- .github/workflows/typescript-api-compat.yml
targets: [job:typescript-api-compat]
reason: checked-in suppression baselines are not compiled items (Layer 1 blind); Hosting/Cli production projects are in affected_project_rules
# A checked-in *.ats.txt baseline changes EXACTLY when an integration's [AspireExport] surface
# changes -- the same surface the per-language polyglot playground scripts regenerate and compile
# (aspire restore --apphost over tests/PolyglotAppHosts/<integration>/<lang>). So an exported-surface
# change must run BOTH typescript-api-compat (baseline diff) AND polyglot (regenerate+compile in every
# language), even when the author has not also touched the tests/PolyglotAppHosts fixtures. Layer 1 can
# attribute the baseline by project-directory containment, but this explicit rule preserves both jobs
# when Layer 1 is skipped and records the runtime polyglot dependency.
- paths:
- src/Aspire.Hosting*/api/*.ats.txt
targets: [job:typescript-api-compat, job:polyglot]
reason: an integration's *.ats.txt baseline tracks its exported ATS surface; the polyglot playground regenerates+compiles that surface per language, so a baseline change must run polyglot too
# Checked-in API/ATS baselines under src/*/api/ (both *.cs and *.ats.txt) are generated
# release artifacts. They are dropped by the prefilter (ci-skip-entirely-patterns.txt) when
# they are the only changed files, so a baseline-only PR skips CI entirely. When an *.ats.txt
# baseline changes alongside real source (e.g. the integration whose [AspireExport] surface it
# tracks), the source change is not blind to Layer 1/the project rules above and routes normally
Comment thread
radical marked this conversation as resolved.
# -- no dedicated rule is needed here for the mixed-change case.
- paths:
- extension/**
- .vscode/launch.json
Expand Down Expand Up @@ -442,8 +437,59 @@ affected_project_rules:
- Aspire.Hosting.JavaScript
- Aspire.Hosting.Python
- Aspire.Hosting.Rust
- Aspire.Hosting
- Aspire.Hosting.Azure
- Aspire.Hosting.Azure.AppConfiguration
- Aspire.Hosting.Azure.AppContainers
- Aspire.Hosting.Azure.AppService
- Aspire.Hosting.Azure.ApplicationInsights
- Aspire.Hosting.Azure.CognitiveServices
- Aspire.Hosting.Azure.ContainerRegistry
- Aspire.Hosting.Azure.CosmosDB
- Aspire.Hosting.Azure.EventHubs
- Aspire.Hosting.Azure.Functions
- Aspire.Hosting.Azure.KeyVault
- Aspire.Hosting.Azure.Kusto
- Aspire.Hosting.Azure.Network
- Aspire.Hosting.Azure.OperationalInsights
- Aspire.Hosting.Azure.PostgreSQL
- Aspire.Hosting.Azure.Redis
- Aspire.Hosting.Azure.Sandboxes
- Aspire.Hosting.Azure.Search
- Aspire.Hosting.Azure.ServiceBus
- Aspire.Hosting.Azure.SignalR
- Aspire.Hosting.Azure.Sql
- Aspire.Hosting.Azure.Storage
- Aspire.Hosting.Azure.WebPubSub
- Aspire.Hosting.Blazor
- Aspire.Hosting.DevTunnels
- Aspire.Hosting.Docker
- Aspire.Hosting.Dotnet
- Aspire.Hosting.EntityFrameworkCore
- Aspire.Hosting.Foundry
- Aspire.Hosting.Garnet
- Aspire.Hosting.GitHub.Models
- Aspire.Hosting.Kafka
- Aspire.Hosting.Keycloak
- Aspire.Hosting.Kubernetes
- Aspire.Hosting.Maui
- Aspire.Hosting.Milvus
- Aspire.Hosting.MongoDB
- Aspire.Hosting.MySql
- Aspire.Hosting.Nats
- Aspire.Hosting.OpenAI
- Aspire.Hosting.Oracle
- Aspire.Hosting.Orleans
- Aspire.Hosting.PostgreSQL
- Aspire.Hosting.Qdrant
- Aspire.Hosting.RabbitMQ
- Aspire.Hosting.Redis
- Aspire.Hosting.Seq
- Aspire.Hosting.SqlServer
- Aspire.Hosting.Valkey
- Aspire.Hosting.Yarp
targets: [job:polyglot]
reason: ATS typesystem -> per-language SDK generators -> RemoteHost server + CLI run + fixtures
reason: ATS typesystem and hosting integrations feed the per-language SDK generators and AppHost fixtures
- projects: [Aspire.Hosting.CodeGeneration.TypeScript]
targets: [job:typescript-sdk]
reason: vitest unit tests over generated TS SDK (@aspire/transport, @aspire/base)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -828,6 +828,15 @@ public void PrefilterReadsPatternsFileAndHonorsKeepRouted()
Assert.True(filter.IsExcluded(".vscode/settings.json"));
Assert.True(filter.IsExcluded("pyrightconfig.json"));

// Generated API/ATS baselines under src/*/api/: no PR CI is needed when these are the only
// changed files.
Assert.True(filter.IsExcluded("src/Aspire.Hosting/api/Aspire.Hosting.cs"));
Assert.True(filter.IsExcluded("src/Aspire.Hosting/api/Aspire.Hosting.ats.txt"));
Assert.True(filter.IsExcluded("src/Aspire.Hosting.Redis/api/Aspire.Hosting.Redis.cs"));
Assert.True(filter.IsExcluded("src/Components/Aspire.Azure.AI.Inference/api/Aspire.Azure.AI.Inference.cs"));
Assert.False(filter.IsExcluded("src/Aspire.Hosting/api/Aspire.Hosting.tscompat.suppression.txt"));
Assert.False(filter.IsExcluded("src/Aspire.Cli/Templating/Templates/java-starter/api/Program.cs"));

// Safety carve-outs: NOT dropped, because they can change build/test outcomes -- nested .gitignore
// files are shipped CLI-template assets (Layer 1 / conventions route them to their projects), root
// launch/task files are validated by extension tests, root .gitignore controls discovery of the
Expand All @@ -841,6 +850,7 @@ public void PrefilterReadsPatternsFileAndHonorsKeepRouted()

// Kept: real source the patterns file does not list.
Assert.False(filter.IsExcluded("src/Aspire.Cli/Program.cs"));
Assert.False(filter.IsExcluded("src/Aspire.Hosting/HostingExtensions.cs"));

// keep_routed carve-outs: listed by the patterns file but routed by the selector -> NOT dropped.
Assert.False(filter.IsExcluded(".github/workflows/backport.yml"));
Expand Down Expand Up @@ -1037,29 +1047,40 @@ public void RealMapTestSupportProjectDoesNotTriggerProductionJobs()
Assert.Empty(result.Jobs);
}

// An integration's checked-in *.ats.txt baseline tracks its exported [AspireExport] surface -- the
// same surface the per-language polyglot playground scripts regenerate and compile
// (aspire restore --apphost over tests/PolyglotAppHosts/<integration>/<lang>). A change to that
// baseline must therefore run BOTH typescript-api-compat (baseline diff) AND polyglot (regenerate +
// compile in every language), so a breaking surface change is caught even if the author did not also
// touch the tests/PolyglotAppHosts fixtures. Run with --skip-layer1 semantics (no Layer 1 affected
// set) to prove the curated layer independently preserves both targets.
// A checked-in API/ATS baseline under src/*/api/ is a generated release artifact: the top-level
// prefilter drops it (ci-skip-entirely-patterns.txt) before either layer runs, so a baseline-ONLY
// change selects nothing. When an integration's real source ALSO changes in the same PR, the
// baseline is still dropped by the prefilter, but the source file reaches Layer 1/the selector
// untouched and is selected via the normal project/path rules. This proves the mixed-change case
// still routes CI even though the dedicated *.ats.txt path rule was removed. Filters the changed
// files through ChangedFileFilter first, mirroring how Program.cs feeds Select (see RunCore).
[Fact]
public void RealMapIntegrationAtsBaselineChangeRunsTypeScriptApiCompatAndPolyglot()
public void RealMapMixedAtsBaselineAndSourceChangeStillRoutesSourceNormally()
{
var mapPath = Path.Combine(RepoRoot.Path, "eng", "github-ci", "test-trigger-map.yml");
var matrix = EnumerateMatrixTestProjects();
var selector = new TestSelector(mapPath, matrix, LoadProjectDirectories(), EnumerateAllTestProjects());
var map = TriggerMap.Load(mapPath);
var filter = ChangedFileFilter.Create(RepoRoot.Path, map.Prefilter);

var atsBaseline = FirstIntegrationAtsBaselineWithPolyglotFixture();

var r = selector.Select([atsBaseline], [], new SelectorOptions());

Assert.False(r.SelectsAll);
Assert.Contains("job:typescript-api-compat", r.Jobs);
Assert.Contains("job:polyglot", r.Jobs);
Assert.Contains(r.JobCauses["job:typescript-api-compat"], cause => cause.Kind == CauseKind.PathRule);
Assert.Contains(r.JobCauses["job:polyglot"], cause => cause.Kind == CauseKind.PathRule);
Assert.True(filter.IsExcluded(atsBaseline));

// Baseline-only: the prefilter drops the file before Select ever sees it -- selects nothing.
var baselineOnly = selector.Select([], [], new SelectorOptions());
Assert.False(baselineOnly.SelectsAll);
Assert.Empty(baselineOnly.TestProjects);
Assert.Empty(baselineOnly.Jobs);

// Mixed: the baseline is still dropped by the prefilter, but the integration's own source file
// (Layer 1-owned, not a prefilter match) still routes normally through Layer 1 / project rules.
var integrationName = atsBaseline.Split('/')[1];
var sourceFile = $"src/{integrationName}/{integrationName}.csproj";
Assert.False(filter.IsExcluded(sourceFile));
var mixed = selector.Select([sourceFile], [integrationName], new SelectorOptions());
Assert.False(mixed.SelectsAll);
Assert.NotEmpty(mixed.TestProjects.Union(mixed.Jobs));
Assert.Contains("job:polyglot", mixed.Jobs);
}

[Fact]
Expand Down
Loading