diff --git a/docs/ci/test-trigger-map.md b/docs/ci/test-trigger-map.md index 0c42187640f..15a8ae65784 100644 --- a/docs/ci/test-trigger-map.md +++ b/docs/ci/test-trigger-map.md @@ -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/**`, diff --git a/eng/github-ci/ci-skip-entirely-patterns.txt b/eng/github-ci/ci-skip-entirely-patterns.txt index 62d4f73d650..9282e3fa070 100644 --- a/eng/github-ci/ci-skip-entirely-patterns.txt +++ b/eng/github-ci/ci-skip-entirely-patterns.txt @@ -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 diff --git a/eng/github-ci/test-trigger-map.yml b/eng/github-ci/test-trigger-map.yml index f2505394cea..8743beeaf0e 100644 --- a/eng/github-ci/test-trigger-map.yml +++ b/eng/github-ci/test-trigger-map.yml @@ -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//). 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 + # -- no dedicated rule is needed here for the mixed-change case. - paths: - extension/** - .vscode/launch.json @@ -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) diff --git a/tests/Infrastructure.Tests/TestTriggerMap/SelectTestsAcceptanceTests.cs b/tests/Infrastructure.Tests/TestTriggerMap/SelectTestsAcceptanceTests.cs index b5cd8823bda..febce445651 100644 --- a/tests/Infrastructure.Tests/TestTriggerMap/SelectTestsAcceptanceTests.cs +++ b/tests/Infrastructure.Tests/TestTriggerMap/SelectTestsAcceptanceTests.cs @@ -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 @@ -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")); @@ -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//). 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]