From 4ed39335367d1fc280f07c923fb0420850990765 Mon Sep 17 00:00:00 2001 From: Jose Perez Rodriguez Date: Fri, 15 May 2026 20:31:59 -0700 Subject: [PATCH 1/7] Add Aspire AppHost for local inner loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds src/source-indexer.AppHost/ — an Aspire app that emulates the prod indexing pipeline end-to-end locally against samples/MiniRuntime/. Resource graph: - 2x Azurite emulators (stage1Storage, prodStorage) with persistent volumes, plus blob containers (stage1, index-local) modeling the two prod storage accounts. - web: SourceIndexServer project, auto-started, SOURCE_BROWSER_INDEX_PROXY_URL wired to prodStorage/index-local. - Explicit-start pipeline resources: - sample-build: builds samples/MiniRuntime to produce a binlog. - upload-stage1: real UploadIndexStage1 tool, tars+gzips the sample folder + binlog, uploads to stage1. - htmlgenerator: runs HtmlGenerator on the binlog to produce HTML under bin/index/ (net472, invoked via dotnet run). - publish-index: az storage blob upload-batch into prodStorage/index-local. - bootstrap-all: composite command on prodStorage that chains the four pipeline resources in order for one-click first-run UX. Code changes to make Azurite work with the existing services (Option A — connection-string fallback): - AzureBlobFileSystem.cs: if AZURE_STORAGE_CONNECTION_STRING is set, use BlobServiceClient(connectionString) instead of URI+TokenCredential, and parse the container name out of the proxy URL. - UploadIndexStage1/Program.cs: --connection-string option + AZURE_STORAGE_CONNECTION_STRING env fallback; CreateIfNotExistsAsync on the destination container. Other: - samples/MiniRuntime/: tiny net9.0 classlib fixture. - docs/inner-loop.md: how to run it + how it maps to prod. - .gitignore: ignore Azurite + Aspire tool state. - source-indexer.sln: pull AppHost + transitively-referenced projects into the solution. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .gitignore | 12 ++ Directory.Packages.props | 5 +- aspire.config.json | 5 + docs/inner-loop.md | 105 ++++++++++ samples/MiniRuntime/MiniArray.cs | 60 ++++++ samples/MiniRuntime/MiniRuntime.csproj | 16 ++ samples/MiniRuntime/MiniRuntimeInfo.cs | 33 +++ samples/MiniRuntime/MiniSpan.cs | 61 ++++++ samples/MiniRuntime/README.md | 8 + .../Models/AzureBlobFileSystem.cs | 31 +++ src/UploadIndexStage1/Program.cs | 66 +++--- src/source-indexer.AppHost/AppHost.cs | 193 ++++++++++++++++++ .../Properties/launchSettings.json | 29 +++ .../appsettings.Development.json | 8 + src/source-indexer.AppHost/appsettings.json | 9 + src/source-indexer.AppHost/aspire.config.json | 5 + .../source-indexer.AppHost.csproj | 26 +++ src/source-indexer.sln | 85 ++++++++ 18 files changed, 732 insertions(+), 25 deletions(-) create mode 100644 aspire.config.json create mode 100644 docs/inner-loop.md create mode 100644 samples/MiniRuntime/MiniArray.cs create mode 100644 samples/MiniRuntime/MiniRuntime.csproj create mode 100644 samples/MiniRuntime/MiniRuntimeInfo.cs create mode 100644 samples/MiniRuntime/MiniSpan.cs create mode 100644 samples/MiniRuntime/README.md create mode 100644 src/source-indexer.AppHost/AppHost.cs create mode 100644 src/source-indexer.AppHost/Properties/launchSettings.json create mode 100644 src/source-indexer.AppHost/appsettings.Development.json create mode 100644 src/source-indexer.AppHost/appsettings.json create mode 100644 src/source-indexer.AppHost/aspire.config.json create mode 100644 src/source-indexer.AppHost/source-indexer.AppHost.csproj diff --git a/.gitignore b/.gitignore index 23fd924..abd4dde 100644 --- a/.gitignore +++ b/.gitignore @@ -257,3 +257,15 @@ paket-files/ *.binlog *.lscache + +# Aspire inner-loop +.azurite/ +samples/*/bin/sample/ +bin/index/ + + +# Aspire CLI / tooling state +.agents/ +.playwright/ +.modules/ + diff --git a/Directory.Packages.props b/Directory.Packages.props index e17e5e9..3042c0c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,3 +1,6 @@ - + + + + \ No newline at end of file diff --git a/aspire.config.json b/aspire.config.json new file mode 100644 index 0000000..38a0f70 --- /dev/null +++ b/aspire.config.json @@ -0,0 +1,5 @@ +{ + "appHost": { + "language": "csharp" + } +} \ No newline at end of file diff --git a/docs/inner-loop.md b/docs/inner-loop.md new file mode 100644 index 0000000..8f045ef --- /dev/null +++ b/docs/inner-loop.md @@ -0,0 +1,105 @@ +# Local inner loop (Aspire) + +`src/source-indexer.AppHost/` is an [Aspire](https://aspire.dev) AppHost that +emulates the production indexing pipeline end-to-end against a tiny sample +library (`samples/MiniRuntime/`). It lets you exercise the full +binlog → HtmlGenerator → blob → SourceIndexServer chain locally without +pushing to ADO. + +## Prerequisites + +- **.NET SDK 10** (pinned in root `global.json`). +- **Docker / Podman** running locally (Azurite emulators run as containers). +- **Aspire CLI** — see . +- **Azure CLI** (`az`) on PATH — used by the `publish-index` step. + +Then from the repo root: + +```pwsh +aspire start +``` + +The dashboard URL (with login token) is printed to the console. + +## Resource graph + +Auto-started (running as soon as the AppHost is up): + +| Resource | Type | Purpose | +|---|---|---| +| `stage1Storage` | Azurite emulator | Models `netsourceindexstage1` (V2 upstream upload destination). | +| `prodStorage` | Azurite emulator | Models `netsourceindexprod` (final HTML index destination). | +| `stage1` | Blob container under `stage1Storage` | Where `upload-stage1` drops the `.tar.gz`. | +| `index-local` | Blob container under `prodStorage` | Where the generated HTML index is uploaded. The web app reads from here. | +| `web` | `SourceIndexServer` project | ASP.NET Core app. `SOURCE_BROWSER_INDEX_PROXY_URL` is wired to `index-local` so it serves the indexed HTML out of Azurite. | + +Explicit-start (stopped by default — click **Start** in the dashboard): + +| Resource | Type | What it does | +|---|---|---| +| `sample-build` | Executable: `dotnet build /bl:` | Builds `samples/MiniRuntime` and produces `samples/MiniRuntime/bin/sample/msbuild.binlog`. | +| `upload-stage1` | Project: `UploadIndexStage1` | Tars+gzips the sample folder + binlog and uploads it to `stage1`. Real V2 upstream tool — fully dogfooded. | +| `htmlgenerator` | Executable: `dotnet run HtmlGenerator` | Runs `HtmlGenerator` on the binlog from `sample-build` to produce static HTML under `bin/index/`. | +| `publish-index` | Executable: `az storage blob upload-batch` | Uploads `bin/index/index/` to the `index-local` container in `prodStorage`. | + +## One-click bootstrap + +The `prodStorage` resource exposes a custom **bootstrap-all** command that +runs the four pipeline resources in order (`sample-build` → +`upload-stage1` → `htmlgenerator` → `publish-index`) and waits for each one +to finish. This is the easy first-run path: + +1. `aspire start` +2. Open the dashboard. +3. On the `prodStorage` resource, click the **bootstrap-all** command. +4. Wait for it to finish (watch the logs). +5. Open the `web` URL — you should see `MiniRuntime`'s indexed HTML. + +Re-running any individual resource regenerates just that stage. + +## How this maps to prod + +| Prod | Local inner loop | +|---|---| +| V2 upstream repo publishes `.tar.gz` to `netsourceindexstage1/stage1//.tar.gz` (`UploadIndexStage1`) | `upload-stage1` resource → `stage1Storage/stage1` | +| `HtmlGenerator.exe` reads binlog + writes HTML to `bin/index/` (`src/index/index.proj`) | `htmlgenerator` resource (`dotnet run --project HtmlGenerator`) | +| `AzureFileCopy@6` uploads `bin/index/index/*` to `netsourceindexprod/index-/` (`azure-pipelines.yml`) | `publish-index` resource (`az storage blob upload-batch`) → `prodStorage/index-local` | +| App Service slot setting `SOURCE_BROWSER_INDEX_PROXY_URL` flipped to the new container (`deployment/deploy-storage-proxy.ps1`) | `web` resource started with `SOURCE_BROWSER_INDEX_PROXY_URL` env var pointing at `prodStorage/index-local` | + +See `docs/handoff/03-indexing-pipeline.md` and +`docs/handoff/05-azure-pipeline.md` for the full prod mechanics. + +## Auth: Azurite vs prod + +Production uses managed identity (`TokenCredential`) to talk to Azure +Storage. Azurite only speaks shared-key / connection-string auth. To keep +both paths working without a fork, `SourceIndexServer/Models/AzureBlobFileSystem.cs` +and `UploadIndexStage1/Program.cs` check for `AZURE_STORAGE_CONNECTION_STRING` +in the environment: + +- If set → use `BlobServiceClient(connectionString)` (Azurite-friendly). +- If not set → use the original `BlobServiceClient(uri, TokenCredential)` + code path (prod-friendly). + +Aspire injects the connection string automatically via `WithReference(...)` +on the blob resources in the AppHost. Prod is unaffected. + +## Persistence + +The two Azurite emulators use `ContainerLifetime.Persistent` and named +Docker volumes (`source-indexer-stage1-data`, `source-indexer-prod-data`). +Blobs uploaded during a session survive `aspire start` restarts. To wipe +state, remove the volumes via Docker / Podman. + +## Open follow-ups + +- **ServiceDefaults / dashboard telemetry**: `SourceIndexServer` still uses + the legacy `IHostBuilder` + `Startup` bootstrap. Wiring + `AddServiceDefaults()` / `MapDefaultEndpoints()` requires migrating to + the minimal-hosting model first. Until then, the `web` resource won't + contribute traces/metrics to the Aspire dashboard, but everything else + works. +- **Debuggable HtmlGenerator**: it's invoked via `dotnet run --project ...` + rather than a typed `AddProject` reference because `HtmlGenerator` + targets `net472` and can't be `ProjectReference`'d from the `net10` + AppHost. Attach manually if you need to debug it. diff --git a/samples/MiniRuntime/MiniArray.cs b/samples/MiniRuntime/MiniArray.cs new file mode 100644 index 0000000..c5ee1c7 --- /dev/null +++ b/samples/MiniRuntime/MiniArray.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace MiniRuntime; + +/// +/// A minimal heap-backed array wrapper used to demonstrate generics and +/// indexer documentation in the indexed HTML output. +/// +/// The element type stored in the array. +public sealed class MiniArray : IReadOnlyList +{ + private readonly T[] _items; + + /// + /// Initializes a new with the given length. + /// + /// The number of elements the array will hold. + /// + /// Thrown if is negative. + /// + public MiniArray(int length) + { + if (length < 0) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + + _items = new T[length]; + } + + /// + /// Gets or sets the element at the given index. + /// + /// The zero-based index. + public T this[int index] + { + get => _items[index]; + set => _items[index] = value; + } + + /// + /// Gets the number of elements in the array. + /// + public int Count => _items.Length; + + /// + public IEnumerator GetEnumerator() + { + for (int i = 0; i < _items.Length; i++) + { + yield return _items[i]; + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + internal T[] UnsafeBuffer => _items; +} diff --git a/samples/MiniRuntime/MiniRuntime.csproj b/samples/MiniRuntime/MiniRuntime.csproj new file mode 100644 index 0000000..e2760ac --- /dev/null +++ b/samples/MiniRuntime/MiniRuntime.csproj @@ -0,0 +1,16 @@ + + + + net9.0 + MiniRuntime + MiniRuntime + true + enable + enable + $(NoWarn);CS1591 + + false + + + diff --git a/samples/MiniRuntime/MiniRuntimeInfo.cs b/samples/MiniRuntime/MiniRuntimeInfo.cs new file mode 100644 index 0000000..6404938 --- /dev/null +++ b/samples/MiniRuntime/MiniRuntimeInfo.cs @@ -0,0 +1,33 @@ +namespace MiniRuntime; + +/// +/// Provides static information about the MiniRuntime fixture library. +/// +/// +/// This type exists so the source indexer has something with XML doc +/// comments, public surface, and internal helpers to render. +/// +public static class MiniRuntimeInfo +{ + /// + /// Gets the human-readable name of this runtime fixture. + /// + public static string Name => "MiniRuntime"; + + /// + /// Gets the version string for this runtime fixture. + /// + public static string Version => "0.1.0-inner-loop"; + + /// + /// Returns a banner suitable for logging at startup. + /// + /// A formatted banner string. + public static string GetBanner() => Banners.Format(Name, Version); + + internal static class Banners + { + internal static string Format(string name, string version) + => $"=== {name} v{version} ==="; + } +} diff --git a/samples/MiniRuntime/MiniSpan.cs b/samples/MiniRuntime/MiniSpan.cs new file mode 100644 index 0000000..1f24d31 --- /dev/null +++ b/samples/MiniRuntime/MiniSpan.cs @@ -0,0 +1,61 @@ +using System; + +namespace MiniRuntime; + +/// +/// A trivial slice-like view over a . +/// +/// The element type. +public readonly struct MiniSpan +{ + private readonly MiniArray _source; + private readonly int _start; + private readonly int _length; + + /// + /// Initializes a new over the given array slice. + /// + /// The backing array. + /// The inclusive start index. + /// The slice length. + public MiniSpan(MiniArray source, int start, int length) + { + ArgumentNullException.ThrowIfNull(source); + + if ((uint)start > (uint)source.Count) + { + throw new ArgumentOutOfRangeException(nameof(start)); + } + + if ((uint)length > (uint)(source.Count - start)) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + + _source = source; + _start = start; + _length = length; + } + + /// Gets the length of the span. + public int Length => _length; + + /// Gets a reference to the element at the given offset. + /// The zero-based offset within the span. + public T this[int index] + { + get + { + if ((uint)index >= (uint)_length) + { + throw new IndexOutOfRangeException(); + } + + return _source[_start + index]; + } + } + + /// Slices this span further. + public MiniSpan Slice(int start, int length) => + new MiniSpan(_source, _start + start, length); +} diff --git a/samples/MiniRuntime/README.md b/samples/MiniRuntime/README.md new file mode 100644 index 0000000..704fc9f --- /dev/null +++ b/samples/MiniRuntime/README.md @@ -0,0 +1,8 @@ +# MiniRuntime + +A tiny stand-in for a "real" upstream repo (think `dotnet/runtime`) used as a +fixture by the local Aspire inner loop in `src/source-indexer.AppHost`. + +It exists purely so the inner loop has something small to build, capture a +binlog from, push to Azurite, and index with `HtmlGenerator`. It is not +published anywhere and is not part of the source-indexer product. diff --git a/src/SourceBrowser/src/SourceIndexServer/Models/AzureBlobFileSystem.cs b/src/SourceBrowser/src/SourceIndexServer/Models/AzureBlobFileSystem.cs index 5b380a7..1005745 100644 --- a/src/SourceBrowser/src/SourceIndexServer/Models/AzureBlobFileSystem.cs +++ b/src/SourceBrowser/src/SourceIndexServer/Models/AzureBlobFileSystem.cs @@ -16,6 +16,19 @@ public class AzureBlobFileSystem : IFileSystem public AzureBlobFileSystem(string uri) { + // Local-dev path: when AZURE_STORAGE_CONNECTION_STRING is set (e.g. by + // the Aspire inner-loop AppHost wiring this service to an Azurite + // emulator), use shared-key auth from the connection string. The + // container name is still taken from the last path segment of `uri` + // so the existing SOURCE_BROWSER_INDEX_PROXY_URL contract is preserved. + string? connectionString = Environment.GetEnvironmentVariable("AZURE_STORAGE_CONNECTION_STRING"); + if (!string.IsNullOrEmpty(connectionString)) + { + string containerName = GetContainerNameFromUri(uri); + container = new BlobContainerClient(connectionString, containerName); + return; + } + var clientId = Environment.GetEnvironmentVariable("AZURE_CLIENT_ID"); credential = string.IsNullOrEmpty(clientId) ? new AzureCliCredential() @@ -25,6 +38,24 @@ public AzureBlobFileSystem(string uri) credential); } + private static string GetContainerNameFromUri(string uri) + { + var parsed = new Uri(uri); + // Azurite URLs are http://host:port/devstoreaccount1/, real + // Azure URLs are https://.blob.core.windows.net/. + // In both cases the last non-empty path segment is the container. + string[] segments = parsed.AbsolutePath + .Split('/', StringSplitOptions.RemoveEmptyEntries); + if (segments.Length == 0) + { + throw new ArgumentException( + $"URI '{uri}' has no path segments; cannot derive container name.", + nameof(uri)); + } + + return segments[segments.Length - 1]; + } + public bool DirectoryExists(string name) { return true; diff --git a/src/UploadIndexStage1/Program.cs b/src/UploadIndexStage1/Program.cs index 3b3d82a..cbd7e7d 100644 --- a/src/UploadIndexStage1/Program.cs +++ b/src/UploadIndexStage1/Program.cs @@ -25,6 +25,7 @@ static async Task Main(string[] args) string clientId = null; string storageAccount = null; string blobContainer = null; + string connectionString = null; var options = new OptionSet { {"i=", "The source folder", i => sourceFolder = i}, @@ -32,6 +33,7 @@ static async Task Main(string[] args) {"c=", "The Azure Client ID (optional)", c => clientId = c}, {"s=", "The destination storage account name or URL", s => storageAccount = s}, {"b=", "The destination storage account container", b => blobContainer = b}, + {"connection-string=", "Optional connection string for local-dev (Azurite) auth. When set, -s/-c are ignored.", cs => connectionString = cs}, }; List extra = options.Parse(args); @@ -51,9 +53,9 @@ static async Task Main(string[] args) Fatal("Missing argument -n"); } - if (string.IsNullOrEmpty(storageAccount)) + if (string.IsNullOrEmpty(connectionString)) { - Fatal("Missing argument -s"); + connectionString = Environment.GetEnvironmentVariable("AZURE_STORAGE_CONNECTION_STRING"); } if (string.IsNullOrEmpty(blobContainer)) @@ -61,37 +63,53 @@ static async Task Main(string[] args) Fatal("Missing argument -b"); } - if (!storageAccount.StartsWith("https://")) - { - storageAccount = "https://" + storageAccount + ".blob.core.windows.net"; - } - + BlobServiceClient blobServiceClient; using AzureEventSourceListener listener = AzureEventSourceListener.CreateConsoleLogger(); - TokenCredential credential; - - if (string.IsNullOrEmpty(clientId) && !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("ARM_CLIENT_ID"))) - { - clientId = Environment.GetEnvironmentVariable("ARM_CLIENT_ID"); - System.Console.WriteLine("Found client ID in environment variable; using it"); - } - - if (string.IsNullOrEmpty(clientId)) + if (!string.IsNullOrEmpty(connectionString)) { - credential = new AzureCliCredential(); - System.Console.WriteLine("Trying to use managed identity without default identity"); + Console.WriteLine("Using AZURE_STORAGE_CONNECTION_STRING / --connection-string for blob auth (local-dev / Azurite path)."); + blobServiceClient = new BlobServiceClient(connectionString); } else { - System.Console.WriteLine("Trying to use ManagedIdentityCredential with ClientID"); - credential = new ManagedIdentityCredential(clientId); - } + if (string.IsNullOrEmpty(storageAccount)) + { + Fatal("Missing argument -s (or supply --connection-string / AZURE_STORAGE_CONNECTION_STRING)"); + } - BlobServiceClient blobServiceClient = new( - new Uri(storageAccount), - credential); + if (!storageAccount.StartsWith("https://")) + { + storageAccount = "https://" + storageAccount + ".blob.core.windows.net"; + } + + TokenCredential credential; + + if (string.IsNullOrEmpty(clientId) && !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("ARM_CLIENT_ID"))) + { + clientId = Environment.GetEnvironmentVariable("ARM_CLIENT_ID"); + System.Console.WriteLine("Found client ID in environment variable; using it"); + } + + if (string.IsNullOrEmpty(clientId)) + { + credential = new AzureCliCredential(); + System.Console.WriteLine("Trying to use managed identity without default identity"); + } + else + { + System.Console.WriteLine("Trying to use ManagedIdentityCredential with ClientID"); + credential = new ManagedIdentityCredential(clientId); + } + + blobServiceClient = new BlobServiceClient( + new Uri(storageAccount), + credential); + } var containerClient = blobServiceClient.GetBlobContainerClient(blobContainer); + // Ensure the container exists (Azurite emulators start empty). + await containerClient.CreateIfNotExistsAsync(); string newBlobName = $"{repoName}/{DateTime.UtcNow:O}.tar.gz"; BlobClient newBlobClient = containerClient.GetBlobClient(newBlobName); diff --git a/src/source-indexer.AppHost/AppHost.cs b/src/source-indexer.AppHost/AppHost.cs new file mode 100644 index 0000000..a275f51 --- /dev/null +++ b/src/source-indexer.AppHost/AppHost.cs @@ -0,0 +1,193 @@ +using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Azure; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +var builder = DistributedApplication.CreateBuilder(args); + +// ============================================================================= +// Storage — emulates the two prod storage accounts via Azurite. +// +// Prod has TWO storage accounts: +// netsourceindexstage1 ← V2 upstream repos push /.tar.gz here +// netsourceindexprod ← HtmlGenerator's output is uploaded as index-/ +// +// We model each as its own AddAzureStorage().RunAsEmulator() (= two Azurite +// containers) so the boundary is visually distinct in the dashboard. +// ============================================================================= + +var stage1Storage = builder.AddAzureStorage("stage1Storage") + .RunAsEmulator(azurite => + { + azurite.WithLifetime(ContainerLifetime.Persistent) + .WithDataVolume("source-indexer-stage1-data"); + }); + +var stage1Blobs = stage1Storage.AddBlobs("stage1-blobs"); +var stage1Container = stage1Storage.AddBlobContainer("stage1", blobContainerName: "stage1"); + +var prodStorage = builder.AddAzureStorage("prodStorage") + .RunAsEmulator(azurite => + { + azurite.WithLifetime(ContainerLifetime.Persistent) + .WithDataVolume("source-indexer-prod-data"); + }); + +var prodBlobs = prodStorage.AddBlobs("prod-blobs"); +var indexContainer = prodStorage.AddBlobContainer("index-local", blobContainerName: "index-local"); + +// ============================================================================= +// Pipeline resources — all WithExplicitStart() so they only run when the user +// clicks "Start" in the dashboard (or via the bootstrap-all composite command). +// They emulate the prod stages from azure-pipelines.yml / src/index/index.proj. +// ============================================================================= + +string repoRoot = Path.GetFullPath(Path.Combine(builder.AppHostDirectory, "..", "..")); +string sampleProj = Path.Combine(repoRoot, "samples", "MiniRuntime", "MiniRuntime.csproj"); +string sampleBinDir = Path.Combine(repoRoot, "samples", "MiniRuntime", "bin", "sample"); +string sampleBinlog = Path.Combine(sampleBinDir, "msbuild.binlog"); +string sampleSourceDir = Path.Combine(repoRoot, "samples", "MiniRuntime"); +string indexOutDir = Path.Combine(repoRoot, "bin", "index"); +string indexUploadDir = Path.Combine(indexOutDir, "index"); +string htmlGeneratorProj = Path.Combine(repoRoot, "src", "SourceBrowser", "src", "HtmlGenerator", "HtmlGenerator.csproj"); + +// 1. sample-build — `dotnet build /bl:` on MiniRuntime, producing a binlog. +// Emulates: Arcade-driven V1 repo build that produces a binlog. +var sampleBuild = builder.AddExecutable( + "sample-build", + "dotnet", + repoRoot, + "build", + sampleProj, + $"/bl:{sampleBinlog}", + "-c", "Debug") + .WithExplicitStart(); + +// 2. upload-stage1 — runs the real UploadIndexStage1 tool against Azurite. +// Emulates: V2 upstream repos uploading their source bundle to +// netsourceindexstage1/stage1//.tar.gz. +// This is the same .NET console app that runs in upstream pipelines, so +// you can attach a debugger to it directly from the Aspire dashboard. +var uploadStage1 = builder.AddProject("upload-stage1") + .WithExplicitStart() + .WithReference(stage1Blobs) + .WaitFor(stage1Container) + .WithEnvironment("AZURE_STORAGE_CONNECTION_STRING", stage1Blobs.Resource.ConnectionStringExpression) + .WithArgs( + "-i", sampleSourceDir, + "-n", "MiniRuntime", + "-b", "stage1"); + +// 3. htmlgenerator — runs HtmlGenerator (net472) over the binlog from step 1 +// and produces static HTML under bin/index/. Modelled via AddExecutable +// (`dotnet run --project HtmlGenerator.csproj`) because net472 can't be +// referenced from a net10 AppHost project, but `dotnet run` still works +// on Windows. Args mirror the /in: and /out: shape from index.proj. +var htmlGenerator = builder.AddExecutable( + "htmlgenerator", + "dotnet", + repoRoot, + "run", + "--project", htmlGeneratorProj, + "-c", "Debug", + "--", + sampleBinlog, + $"/out:{indexOutDir}", + $"/serverPath:{sampleSourceDir}=https://github.com/dotnet/source-indexer/tree/main/samples/MiniRuntime/") + .WithExplicitStart(); + +// 4. publish-index — wraps the Azure CLI (matching prod's AzureFileCopy@6 +// task). Uploads bin/index/index/* to prodStorage/index-local. +var publishIndex = builder.AddExecutable( + "publish-index", + "az", + repoRoot, + "storage", "blob", "upload-batch", + "-s", indexUploadDir, + "-d", "index-local", + "--overwrite", + "true") + .WithExplicitStart() + .WithReference(prodBlobs) + .WaitFor(indexContainer) + .WithEnvironment(ctx => + { + // The az CLI reads AZURE_STORAGE_CONNECTION_STRING natively. + ctx.EnvironmentVariables["AZURE_STORAGE_CONNECTION_STRING"] = + prodBlobs.Resource.ConnectionStringExpression; + }); + +// ============================================================================= +// Web — SourceIndexServer, the real app. Auto-starts and serves indexed HTML +// out of prodStorage/index-local via SOURCE_BROWSER_INDEX_PROXY_URL (the same +// env var contract prod uses; see deployment/deploy-storage-proxy.ps1). +// ============================================================================= + +var web = builder.AddProject("web") + .WithReference(prodBlobs) + .WaitFor(indexContainer) + .WithEnvironment("AZURE_STORAGE_CONNECTION_STRING", prodBlobs.Resource.ConnectionStringExpression) + .WithEnvironment("SOURCE_BROWSER_INDEX_PROXY_URL", indexContainer.Resource.ConnectionStringExpression) + .WithExternalHttpEndpoints(); + +// ============================================================================= +// bootstrap-all — composite command that runs the full pipeline end-to-end. +// Attached to prodStorage (the "end of the pipeline" resource) since +// app-host-level commands aren't yet available in this Aspire SDK. +// ============================================================================= + +prodStorage.WithCommand( + "bootstrap-all", + "Bootstrap full pipeline", + async context => + { + var ct = context.CancellationToken; + var commandService = context.ServiceProvider.GetRequiredService(); + var notifications = context.ServiceProvider.GetRequiredService(); + var logger = context.Logger; + + var stages = new (string Name, IResource Resource)[] + { + ("sample-build", sampleBuild.Resource), + ("upload-stage1", uploadStage1.Resource), + ("htmlgenerator", htmlGenerator.Resource), + ("publish-index", publishIndex.Resource), + }; + + foreach (var (name, resource) in stages) + { + logger.LogInformation("[bootstrap-all] Starting {Stage}...", name); + + var start = await commandService.ExecuteCommandAsync( + resource: resource, + commandName: "resource-start", + cancellationToken: ct); + + if (!start.Success) + { + return CommandResults.Failure($"Failed to start {name}: {start.Message}"); + } + + // Wait until the resource reaches a terminal state. + var snapshot = await notifications.WaitForResourceAsync( + resource.Name, + e => e.Snapshot.State?.Text is "Finished" or "Exited" or "FailedToStart" or "RuntimeUnhealthy", + ct); + + var state = snapshot.Snapshot.State?.Text; + var exitCode = snapshot.Snapshot.ExitCode; + + if (state != "Finished" && state != "Exited" || (exitCode is int code && code != 0)) + { + return CommandResults.Failure( + $"{name} ended in state '{state}' with exit code {exitCode?.ToString() ?? ""}."); + } + + logger.LogInformation("[bootstrap-all] {Stage} finished cleanly.", name); + } + + return CommandResults.Success(); + }); + +builder.Build().Run(); + diff --git a/src/source-indexer.AppHost/Properties/launchSettings.json b/src/source-indexer.AppHost/Properties/launchSettings.json new file mode 100644 index 0000000..33d3c81 --- /dev/null +++ b/src/source-indexer.AppHost/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:17258;http://localhost:15161", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21208", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22007" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15161", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19226", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20243" + } + } + } +} diff --git a/src/source-indexer.AppHost/appsettings.Development.json b/src/source-indexer.AppHost/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/src/source-indexer.AppHost/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/src/source-indexer.AppHost/appsettings.json b/src/source-indexer.AppHost/appsettings.json new file mode 100644 index 0000000..31c092a --- /dev/null +++ b/src/source-indexer.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/src/source-indexer.AppHost/aspire.config.json b/src/source-indexer.AppHost/aspire.config.json new file mode 100644 index 0000000..0e212ac --- /dev/null +++ b/src/source-indexer.AppHost/aspire.config.json @@ -0,0 +1,5 @@ +{ + "appHost": { + "path": "source-indexer.AppHost.csproj" + } +} diff --git a/src/source-indexer.AppHost/source-indexer.AppHost.csproj b/src/source-indexer.AppHost/source-indexer.AppHost.csproj new file mode 100644 index 0000000..1af3491 --- /dev/null +++ b/src/source-indexer.AppHost/source-indexer.AppHost.csproj @@ -0,0 +1,26 @@ + + + + Exe + net10.0 + enable + enable + fc6b83b7-dced-49f7-aa1c-78458fd03ee3 + + false + + + + + + + + + + + + diff --git a/src/source-indexer.sln b/src/source-indexer.sln index a9dce76..45070de 100644 --- a/src/source-indexer.sln +++ b/src/source-indexer.sln @@ -7,24 +7,109 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.SourceIndexer.Tas EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UploadIndexStage1", "UploadIndexStage1\UploadIndexStage1.csproj", "{72B1789D-B80E-4073-BDB4-672E3E2E45DA}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "source-indexer.AppHost", "source-indexer.AppHost\source-indexer.AppHost.csproj", "{8C2DD96E-D6A0-40F5-87AE-D3A2E97E1627}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SourceBrowser", "SourceBrowser", "{F69B90C1-642B-7466-139B-A7D79630D179}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{61FFE273-1437-3EAF-DA5A-B4D8CAE2E20F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SourceIndexServer", "SourceBrowser\src\SourceIndexServer\SourceIndexServer.csproj", "{EB2C8F73-63CD-4727-B11D-773E9E0A1E40}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common", "SourceBrowser\src\Common\Common.csproj", "{623DF7AB-9F20-41CA-B1A9-10C100028A17}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniRuntime", "..\samples\MiniRuntime\MiniRuntime.csproj", "{1F66CD86-D2AD-40C2-846A-292FBB020CF3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {5D86D28A-FEB9-4561-8EA8-D3C3BB409FB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5D86D28A-FEB9-4561-8EA8-D3C3BB409FB3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5D86D28A-FEB9-4561-8EA8-D3C3BB409FB3}.Debug|x64.ActiveCfg = Debug|Any CPU + {5D86D28A-FEB9-4561-8EA8-D3C3BB409FB3}.Debug|x64.Build.0 = Debug|Any CPU + {5D86D28A-FEB9-4561-8EA8-D3C3BB409FB3}.Debug|x86.ActiveCfg = Debug|Any CPU + {5D86D28A-FEB9-4561-8EA8-D3C3BB409FB3}.Debug|x86.Build.0 = Debug|Any CPU {5D86D28A-FEB9-4561-8EA8-D3C3BB409FB3}.Release|Any CPU.ActiveCfg = Release|Any CPU {5D86D28A-FEB9-4561-8EA8-D3C3BB409FB3}.Release|Any CPU.Build.0 = Release|Any CPU + {5D86D28A-FEB9-4561-8EA8-D3C3BB409FB3}.Release|x64.ActiveCfg = Release|Any CPU + {5D86D28A-FEB9-4561-8EA8-D3C3BB409FB3}.Release|x64.Build.0 = Release|Any CPU + {5D86D28A-FEB9-4561-8EA8-D3C3BB409FB3}.Release|x86.ActiveCfg = Release|Any CPU + {5D86D28A-FEB9-4561-8EA8-D3C3BB409FB3}.Release|x86.Build.0 = Release|Any CPU {72B1789D-B80E-4073-BDB4-672E3E2E45DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {72B1789D-B80E-4073-BDB4-672E3E2E45DA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {72B1789D-B80E-4073-BDB4-672E3E2E45DA}.Debug|x64.ActiveCfg = Debug|Any CPU + {72B1789D-B80E-4073-BDB4-672E3E2E45DA}.Debug|x64.Build.0 = Debug|Any CPU + {72B1789D-B80E-4073-BDB4-672E3E2E45DA}.Debug|x86.ActiveCfg = Debug|Any CPU + {72B1789D-B80E-4073-BDB4-672E3E2E45DA}.Debug|x86.Build.0 = Debug|Any CPU {72B1789D-B80E-4073-BDB4-672E3E2E45DA}.Release|Any CPU.ActiveCfg = Release|Any CPU {72B1789D-B80E-4073-BDB4-672E3E2E45DA}.Release|Any CPU.Build.0 = Release|Any CPU + {72B1789D-B80E-4073-BDB4-672E3E2E45DA}.Release|x64.ActiveCfg = Release|Any CPU + {72B1789D-B80E-4073-BDB4-672E3E2E45DA}.Release|x64.Build.0 = Release|Any CPU + {72B1789D-B80E-4073-BDB4-672E3E2E45DA}.Release|x86.ActiveCfg = Release|Any CPU + {72B1789D-B80E-4073-BDB4-672E3E2E45DA}.Release|x86.Build.0 = Release|Any CPU + {8C2DD96E-D6A0-40F5-87AE-D3A2E97E1627}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8C2DD96E-D6A0-40F5-87AE-D3A2E97E1627}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8C2DD96E-D6A0-40F5-87AE-D3A2E97E1627}.Debug|x64.ActiveCfg = Debug|Any CPU + {8C2DD96E-D6A0-40F5-87AE-D3A2E97E1627}.Debug|x64.Build.0 = Debug|Any CPU + {8C2DD96E-D6A0-40F5-87AE-D3A2E97E1627}.Debug|x86.ActiveCfg = Debug|Any CPU + {8C2DD96E-D6A0-40F5-87AE-D3A2E97E1627}.Debug|x86.Build.0 = Debug|Any CPU + {8C2DD96E-D6A0-40F5-87AE-D3A2E97E1627}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8C2DD96E-D6A0-40F5-87AE-D3A2E97E1627}.Release|Any CPU.Build.0 = Release|Any CPU + {8C2DD96E-D6A0-40F5-87AE-D3A2E97E1627}.Release|x64.ActiveCfg = Release|Any CPU + {8C2DD96E-D6A0-40F5-87AE-D3A2E97E1627}.Release|x64.Build.0 = Release|Any CPU + {8C2DD96E-D6A0-40F5-87AE-D3A2E97E1627}.Release|x86.ActiveCfg = Release|Any CPU + {8C2DD96E-D6A0-40F5-87AE-D3A2E97E1627}.Release|x86.Build.0 = Release|Any CPU + {EB2C8F73-63CD-4727-B11D-773E9E0A1E40}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EB2C8F73-63CD-4727-B11D-773E9E0A1E40}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EB2C8F73-63CD-4727-B11D-773E9E0A1E40}.Debug|x64.ActiveCfg = Debug|Any CPU + {EB2C8F73-63CD-4727-B11D-773E9E0A1E40}.Debug|x64.Build.0 = Debug|Any CPU + {EB2C8F73-63CD-4727-B11D-773E9E0A1E40}.Debug|x86.ActiveCfg = Debug|Any CPU + {EB2C8F73-63CD-4727-B11D-773E9E0A1E40}.Debug|x86.Build.0 = Debug|Any CPU + {EB2C8F73-63CD-4727-B11D-773E9E0A1E40}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EB2C8F73-63CD-4727-B11D-773E9E0A1E40}.Release|Any CPU.Build.0 = Release|Any CPU + {EB2C8F73-63CD-4727-B11D-773E9E0A1E40}.Release|x64.ActiveCfg = Release|Any CPU + {EB2C8F73-63CD-4727-B11D-773E9E0A1E40}.Release|x64.Build.0 = Release|Any CPU + {EB2C8F73-63CD-4727-B11D-773E9E0A1E40}.Release|x86.ActiveCfg = Release|Any CPU + {EB2C8F73-63CD-4727-B11D-773E9E0A1E40}.Release|x86.Build.0 = Release|Any CPU + {623DF7AB-9F20-41CA-B1A9-10C100028A17}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {623DF7AB-9F20-41CA-B1A9-10C100028A17}.Debug|Any CPU.Build.0 = Debug|Any CPU + {623DF7AB-9F20-41CA-B1A9-10C100028A17}.Debug|x64.ActiveCfg = Debug|Any CPU + {623DF7AB-9F20-41CA-B1A9-10C100028A17}.Debug|x64.Build.0 = Debug|Any CPU + {623DF7AB-9F20-41CA-B1A9-10C100028A17}.Debug|x86.ActiveCfg = Debug|Any CPU + {623DF7AB-9F20-41CA-B1A9-10C100028A17}.Debug|x86.Build.0 = Debug|Any CPU + {623DF7AB-9F20-41CA-B1A9-10C100028A17}.Release|Any CPU.ActiveCfg = Release|Any CPU + {623DF7AB-9F20-41CA-B1A9-10C100028A17}.Release|Any CPU.Build.0 = Release|Any CPU + {623DF7AB-9F20-41CA-B1A9-10C100028A17}.Release|x64.ActiveCfg = Release|Any CPU + {623DF7AB-9F20-41CA-B1A9-10C100028A17}.Release|x64.Build.0 = Release|Any CPU + {623DF7AB-9F20-41CA-B1A9-10C100028A17}.Release|x86.ActiveCfg = Release|Any CPU + {623DF7AB-9F20-41CA-B1A9-10C100028A17}.Release|x86.Build.0 = Release|Any CPU + {1F66CD86-D2AD-40C2-846A-292FBB020CF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1F66CD86-D2AD-40C2-846A-292FBB020CF3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1F66CD86-D2AD-40C2-846A-292FBB020CF3}.Debug|x64.ActiveCfg = Debug|Any CPU + {1F66CD86-D2AD-40C2-846A-292FBB020CF3}.Debug|x64.Build.0 = Debug|Any CPU + {1F66CD86-D2AD-40C2-846A-292FBB020CF3}.Debug|x86.ActiveCfg = Debug|Any CPU + {1F66CD86-D2AD-40C2-846A-292FBB020CF3}.Debug|x86.Build.0 = Debug|Any CPU + {1F66CD86-D2AD-40C2-846A-292FBB020CF3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1F66CD86-D2AD-40C2-846A-292FBB020CF3}.Release|Any CPU.Build.0 = Release|Any CPU + {1F66CD86-D2AD-40C2-846A-292FBB020CF3}.Release|x64.ActiveCfg = Release|Any CPU + {1F66CD86-D2AD-40C2-846A-292FBB020CF3}.Release|x64.Build.0 = Release|Any CPU + {1F66CD86-D2AD-40C2-846A-292FBB020CF3}.Release|x86.ActiveCfg = Release|Any CPU + {1F66CD86-D2AD-40C2-846A-292FBB020CF3}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {61FFE273-1437-3EAF-DA5A-B4D8CAE2E20F} = {F69B90C1-642B-7466-139B-A7D79630D179} + {EB2C8F73-63CD-4727-B11D-773E9E0A1E40} = {61FFE273-1437-3EAF-DA5A-B4D8CAE2E20F} + {623DF7AB-9F20-41CA-B1A9-10C100028A17} = {61FFE273-1437-3EAF-DA5A-B4D8CAE2E20F} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {CBA6774C-91B0-4634-94B4-B6C12A4519C8} EndGlobalSection From 015750f627c8fd154bbdecb9ec8546d372233cf0 Mon Sep 17 00:00:00 2001 From: Jose Perez Rodriguez Date: Fri, 15 May 2026 22:15:22 -0700 Subject: [PATCH 2/7] Address PR feedback and fix index loading post-bootstrap - Genericize AzureBlobFileSystem connection-string init (no Aspire framing) - Add --connection-string option + AZURE_STORAGE_CONNECTION_STRING fallback to UploadIndexStage1 - bootstrap-all now restarts the web resource after step5 so the IndexLoader re-reads the freshly populated container (local equivalent of the prod slot-setting flip) - Minor AppHost cleanup (resource name prefixes, persistent containers, mounted volumes) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .gitignore | 4 + docs/inner-loop.md | 33 ++++---- .../Models/AzureBlobFileSystem.cs | 23 +++-- src/UploadIndexStage1/Program.cs | 5 +- src/source-indexer.AppHost/AppHost.cs | 83 +++++++++++++------ .../source-indexer.AppHost.csproj | 6 ++ 6 files changed, 102 insertions(+), 52 deletions(-) diff --git a/.gitignore b/.gitignore index abd4dde..91e033c 100644 --- a/.gitignore +++ b/.gitignore @@ -269,3 +269,7 @@ bin/index/ .playwright/ .modules/ + +# HtmlGenerator drops Errors.txt into its working directory on startup. +src/SourceBrowser/src/HtmlGenerator/Errors.txt + diff --git a/docs/inner-loop.md b/docs/inner-loop.md index 8f045ef..656111d 100644 --- a/docs/inner-loop.md +++ b/docs/inner-loop.md @@ -37,17 +37,17 @@ Explicit-start (stopped by default — click **Start** in the dashboard): | Resource | Type | What it does | |---|---|---| -| `sample-build` | Executable: `dotnet build /bl:` | Builds `samples/MiniRuntime` and produces `samples/MiniRuntime/bin/sample/msbuild.binlog`. | -| `upload-stage1` | Project: `UploadIndexStage1` | Tars+gzips the sample folder + binlog and uploads it to `stage1`. Real V2 upstream tool — fully dogfooded. | -| `htmlgenerator` | Executable: `dotnet run HtmlGenerator` | Runs `HtmlGenerator` on the binlog from `sample-build` to produce static HTML under `bin/index/`. | -| `publish-index` | Executable: `az storage blob upload-batch` | Uploads `bin/index/index/` to the `index-local` container in `prodStorage`. | +| `step1-sample-build` | Executable: `dotnet build /bl:` | Builds `samples/MiniRuntime` and produces `samples/MiniRuntime/bin/sample/msbuild.binlog`. | +| `step2-upload-stage1` | Project: `UploadIndexStage1` | Tars+gzips the sample folder + binlog and uploads it to `stage1`. Real V2 upstream tool — fully dogfooded. | +| `step3-htmlgenerator` | Project: `HtmlGenerator` | Runs `HtmlGenerator` on the binlog from `step1-sample-build` to produce static HTML under `bin/index/`. | +| `step4-publish-index` | Executable: `az storage blob upload-batch` | Uploads `bin/index/index/` to the `index-local` container in `prodStorage`. | ## One-click bootstrap The `prodStorage` resource exposes a custom **bootstrap-all** command that -runs the four pipeline resources in order (`sample-build` → -`upload-stage1` → `htmlgenerator` → `publish-index`) and waits for each one -to finish. This is the easy first-run path: +runs the four pipeline resources in order (`step1-sample-build` → +`step2-upload-stage1` → `step3-htmlgenerator` → `step4-publish-index`) and +waits for each one to finish. This is the easy first-run path: 1. `aspire start` 2. Open the dashboard. @@ -61,9 +61,9 @@ Re-running any individual resource regenerates just that stage. | Prod | Local inner loop | |---|---| -| V2 upstream repo publishes `.tar.gz` to `netsourceindexstage1/stage1//.tar.gz` (`UploadIndexStage1`) | `upload-stage1` resource → `stage1Storage/stage1` | -| `HtmlGenerator.exe` reads binlog + writes HTML to `bin/index/` (`src/index/index.proj`) | `htmlgenerator` resource (`dotnet run --project HtmlGenerator`) | -| `AzureFileCopy@6` uploads `bin/index/index/*` to `netsourceindexprod/index-/` (`azure-pipelines.yml`) | `publish-index` resource (`az storage blob upload-batch`) → `prodStorage/index-local` | +| V2 upstream repo publishes `.tar.gz` to `netsourceindexstage1/stage1//.tar.gz` (`UploadIndexStage1`) | `step2-upload-stage1` resource → `stage1Storage/stage1` | +| `HtmlGenerator.exe` reads binlog + writes HTML to `bin/index/` (`src/index/index.proj`) | `step3-htmlgenerator` resource | +| `AzureFileCopy@6` uploads `bin/index/index/*` to `netsourceindexprod/index-/` (`azure-pipelines.yml`) | `step4-publish-index` resource (`az storage blob upload-batch`) → `prodStorage/index-local` | | App Service slot setting `SOURCE_BROWSER_INDEX_PROXY_URL` flipped to the new container (`deployment/deploy-storage-proxy.ps1`) | `web` resource started with `SOURCE_BROWSER_INDEX_PROXY_URL` env var pointing at `prodStorage/index-local` | See `docs/handoff/03-indexing-pipeline.md` and @@ -86,10 +86,11 @@ on the blob resources in the AppHost. Prod is unaffected. ## Persistence -The two Azurite emulators use `ContainerLifetime.Persistent` and named -Docker volumes (`source-indexer-stage1-data`, `source-indexer-prod-data`). -Blobs uploaded during a session survive `aspire start` restarts. To wipe -state, remove the volumes via Docker / Podman. +The two Azurite emulators use `ContainerLifetime.Persistent` and bind-mount +their data directories to `.azurite/stage1` and `.azurite/prod` at the repo +root. Blobs uploaded during a session survive `aspire start` restarts and +container recreations — to wipe state, just delete the `.azurite/` folder. +The folder is gitignored. ## Open follow-ups @@ -99,7 +100,3 @@ state, remove the volumes via Docker / Podman. the minimal-hosting model first. Until then, the `web` resource won't contribute traces/metrics to the Aspire dashboard, but everything else works. -- **Debuggable HtmlGenerator**: it's invoked via `dotnet run --project ...` - rather than a typed `AddProject` reference because `HtmlGenerator` - targets `net472` and can't be `ProjectReference`'d from the `net10` - AppHost. Attach manually if you need to debug it. diff --git a/src/SourceBrowser/src/SourceIndexServer/Models/AzureBlobFileSystem.cs b/src/SourceBrowser/src/SourceIndexServer/Models/AzureBlobFileSystem.cs index 1005745..e502aec 100644 --- a/src/SourceBrowser/src/SourceIndexServer/Models/AzureBlobFileSystem.cs +++ b/src/SourceBrowser/src/SourceIndexServer/Models/AzureBlobFileSystem.cs @@ -16,11 +16,9 @@ public class AzureBlobFileSystem : IFileSystem public AzureBlobFileSystem(string uri) { - // Local-dev path: when AZURE_STORAGE_CONNECTION_STRING is set (e.g. by - // the Aspire inner-loop AppHost wiring this service to an Azurite - // emulator), use shared-key auth from the connection string. The - // container name is still taken from the last path segment of `uri` - // so the existing SOURCE_BROWSER_INDEX_PROXY_URL contract is preserved. + // If AZURE_STORAGE_CONNECTION_STRING is set, initialize the client from + // the connection string. The container name still comes from `uri` so + // the existing SOURCE_BROWSER_INDEX_PROXY_URL contract is preserved. string? connectionString = Environment.GetEnvironmentVariable("AZURE_STORAGE_CONNECTION_STRING"); if (!string.IsNullOrEmpty(connectionString)) { @@ -40,6 +38,21 @@ public AzureBlobFileSystem(string uri) private static string GetContainerNameFromUri(string uri) { + // The connection-string form of `uri` may include a ContainerName= + // segment (e.g. "DefaultEndpointsProtocol=...;ContainerName=index"). + // Honor that segment first so callers can pass a full connection + // string instead of a URL. + if (uri.Contains("ContainerName=", StringComparison.OrdinalIgnoreCase)) + { + foreach (var segment in uri.Split(';', StringSplitOptions.RemoveEmptyEntries)) + { + if (segment.StartsWith("ContainerName=", StringComparison.OrdinalIgnoreCase)) + { + return segment["ContainerName=".Length..].Trim(); + } + } + } + var parsed = new Uri(uri); // Azurite URLs are http://host:port/devstoreaccount1/, real // Azure URLs are https://.blob.core.windows.net/. diff --git a/src/UploadIndexStage1/Program.cs b/src/UploadIndexStage1/Program.cs index cbd7e7d..3ed2445 100644 --- a/src/UploadIndexStage1/Program.cs +++ b/src/UploadIndexStage1/Program.cs @@ -33,7 +33,7 @@ static async Task Main(string[] args) {"c=", "The Azure Client ID (optional)", c => clientId = c}, {"s=", "The destination storage account name or URL", s => storageAccount = s}, {"b=", "The destination storage account container", b => blobContainer = b}, - {"connection-string=", "Optional connection string for local-dev (Azurite) auth. When set, -s/-c are ignored.", cs => connectionString = cs}, + {"connection-string=", "Optional connection string for blob auth. When set, -s/-c are ignored.", cs => connectionString = cs}, }; List extra = options.Parse(args); @@ -68,7 +68,7 @@ static async Task Main(string[] args) if (!string.IsNullOrEmpty(connectionString)) { - Console.WriteLine("Using AZURE_STORAGE_CONNECTION_STRING / --connection-string for blob auth (local-dev / Azurite path)."); + Console.WriteLine("Using connection-string for blob auth."); blobServiceClient = new BlobServiceClient(connectionString); } else @@ -108,7 +108,6 @@ static async Task Main(string[] args) } var containerClient = blobServiceClient.GetBlobContainerClient(blobContainer); - // Ensure the container exists (Azurite emulators start empty). await containerClient.CreateIfNotExistsAsync(); string newBlobName = $"{repoName}/{DateTime.UtcNow:O}.tar.gz"; BlobClient newBlobClient = containerClient.GetBlobClient(newBlobName); diff --git a/src/source-indexer.AppHost/AppHost.cs b/src/source-indexer.AppHost/AppHost.cs index a275f51..30be1ef 100644 --- a/src/source-indexer.AppHost/AppHost.cs +++ b/src/source-indexer.AppHost/AppHost.cs @@ -20,21 +20,21 @@ .RunAsEmulator(azurite => { azurite.WithLifetime(ContainerLifetime.Persistent) - .WithDataVolume("source-indexer-stage1-data"); + .WithDataBindMount(".azurite/stage1"); }); var stage1Blobs = stage1Storage.AddBlobs("stage1-blobs"); -var stage1Container = stage1Storage.AddBlobContainer("stage1", blobContainerName: "stage1"); +var stage1Container = stage1Blobs.AddBlobContainer("stage1", blobContainerName: "stage1"); var prodStorage = builder.AddAzureStorage("prodStorage") .RunAsEmulator(azurite => { azurite.WithLifetime(ContainerLifetime.Persistent) - .WithDataVolume("source-indexer-prod-data"); + .WithDataBindMount(".azurite/prod"); }); var prodBlobs = prodStorage.AddBlobs("prod-blobs"); -var indexContainer = prodStorage.AddBlobContainer("index-local", blobContainerName: "index-local"); +var indexContainer = prodBlobs.AddBlobContainer("index-local", blobContainerName: "index-local"); // ============================================================================= // Pipeline resources — all WithExplicitStart() so they only run when the user @@ -49,17 +49,20 @@ string sampleSourceDir = Path.Combine(repoRoot, "samples", "MiniRuntime"); string indexOutDir = Path.Combine(repoRoot, "bin", "index"); string indexUploadDir = Path.Combine(indexOutDir, "index"); -string htmlGeneratorProj = Path.Combine(repoRoot, "src", "SourceBrowser", "src", "HtmlGenerator", "HtmlGenerator.csproj"); // 1. sample-build — `dotnet build /bl:` on MiniRuntime, producing a binlog. // Emulates: Arcade-driven V1 repo build that produces a binlog. +// Use /t:Rebuild so MSBuild always re-invokes the C# compiler — otherwise an +// incremental no-op build produces a binlog with zero Csc invocations and +// HtmlGenerator finds nothing to index. var sampleBuild = builder.AddExecutable( - "sample-build", + "step1-sample-build", "dotnet", repoRoot, "build", sampleProj, $"/bl:{sampleBinlog}", + "/t:Rebuild", "-c", "Debug") .WithExplicitStart(); @@ -68,7 +71,7 @@ // netsourceindexstage1/stage1//.tar.gz. // This is the same .NET console app that runs in upstream pipelines, so // you can attach a debugger to it directly from the Aspire dashboard. -var uploadStage1 = builder.AddProject("upload-stage1") +var uploadStage1 = builder.AddProject("step2-upload-stage1") .WithExplicitStart() .WithReference(stage1Blobs) .WaitFor(stage1Container) @@ -79,27 +82,39 @@ "-b", "stage1"); // 3. htmlgenerator — runs HtmlGenerator (net472) over the binlog from step 1 -// and produces static HTML under bin/index/. Modelled via AddExecutable -// (`dotnet run --project HtmlGenerator.csproj`) because net472 can't be -// referenced from a net10 AppHost project, but `dotnet run` still works -// on Windows. Args mirror the /in: and /out: shape from index.proj. -var htmlGenerator = builder.AddExecutable( - "htmlgenerator", - "dotnet", - repoRoot, - "run", - "--project", htmlGeneratorProj, - "-c", "Debug", - "--", +// and produces static HTML under bin/index/. HtmlGenerator targets net472 +// so we can't link its output assembly into the net10 AppHost, but +// `ReferenceOutputAssembly="false" SkipGetTargetFrameworkProperties="true"` +// on the ProjectReference still gets us the Projects.HtmlGenerator +// metadata, so we can use AddProject and get debugging / restart-on- +// rebuild for free. +var htmlGenerator = builder.AddProject("step3-htmlgenerator", launchProfileName: null) + .WithExplicitStart() + .WithArgs( sampleBinlog, $"/out:{indexOutDir}", - $"/serverPath:{sampleSourceDir}=https://github.com/dotnet/source-indexer/tree/main/samples/MiniRuntime/") + "/force", + $"/serverPath:{sampleSourceDir}=https://github.com/dotnet/source-indexer/tree/main/samples/MiniRuntime/"); + +// 4. normalize-case — lowercase every filename under bin/index/index/. +// SourceIndexServer.Helpers.ServeProxiedIndex lowercases incoming request +// paths before querying the blob (case-sensitive in Azure Storage), so any +// PascalCase output from HtmlGenerator (Projects.txt, results.html, etc.) +// would 404 if uploaded as-is. Prod runs the same script — see +// azure-pipelines.yml line ~165 (deployment/normalize-case.ps1). +var normalizeCase = builder.AddExecutable( + "step4-normalize-case", + "pwsh", + repoRoot, + "-NoProfile", + "-File", Path.Combine(repoRoot, "deployment", "normalize-case.ps1"), + "-Root", indexUploadDir) .WithExplicitStart(); -// 4. publish-index — wraps the Azure CLI (matching prod's AzureFileCopy@6 +// 5. publish-index — wraps the Azure CLI (matching prod's AzureFileCopy@6 // task). Uploads bin/index/index/* to prodStorage/index-local. var publishIndex = builder.AddExecutable( - "publish-index", + "step5-publish-index", "az", repoRoot, "storage", "blob", "upload-batch", @@ -148,10 +163,11 @@ var stages = new (string Name, IResource Resource)[] { - ("sample-build", sampleBuild.Resource), - ("upload-stage1", uploadStage1.Resource), - ("htmlgenerator", htmlGenerator.Resource), - ("publish-index", publishIndex.Resource), + ("step1-sample-build", sampleBuild.Resource), + ("step2-upload-stage1", uploadStage1.Resource), + ("step3-htmlgenerator", htmlGenerator.Resource), + ("step4-normalize-case", normalizeCase.Resource), + ("step5-publish-index", publishIndex.Resource), }; foreach (var (name, resource) in stages) @@ -186,6 +202,21 @@ logger.LogInformation("[bootstrap-all] {Stage} finished cleanly.", name); } + // The web's IndexLoader only runs once at startup (Index ctor → Task.Run). + // In prod the app-service slot-setting flip restarts the app after publish; + // locally we mirror that by restarting `web` so it re-reads the freshly + // published container. + logger.LogInformation("[bootstrap-all] Restarting web to pick up the freshly published index..."); + var restart = await commandService.ExecuteCommandAsync( + resource: web.Resource, + commandName: "resource-restart", + cancellationToken: ct); + if (!restart.Success) + { + return CommandResults.Failure($"Failed to restart web: {restart.Message}"); + } + await notifications.WaitForResourceHealthyAsync(web.Resource.Name, ct); + return CommandResults.Success(); }); diff --git a/src/source-indexer.AppHost/source-indexer.AppHost.csproj b/src/source-indexer.AppHost/source-indexer.AppHost.csproj index 1af3491..e294598 100644 --- a/src/source-indexer.AppHost/source-indexer.AppHost.csproj +++ b/src/source-indexer.AppHost/source-indexer.AppHost.csproj @@ -21,6 +21,12 @@ + + From 1a8fbd92b2756127f4a93cda032f88238923baa2 Mon Sep 17 00:00:00 2001 From: Jose Perez Rodriguez Date: Fri, 15 May 2026 22:35:01 -0700 Subject: [PATCH 3/7] Add Aspire ServiceDefaults for OTel export to dashboard Wires OpenTelemetry/health/resilience into SourceIndexServer and UploadIndexStage1 so their traces, metrics, and logs flow to the Aspire dashboard's OTLP collector. Migrates SourceIndexServer from the legacy Host.CreateDefaultBuilder + Startup pattern to WebApplication.CreateBuilder so it can call AddServiceDefaults/MapDefaultEndpoints. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Directory.Packages.props | 7 + .../src/SourceIndexServer/Program.cs | 89 +++++++++-- .../SourceIndexServer.csproj | 1 + .../src/SourceIndexServer/Startup.cs | 145 ------------------ src/UploadIndexStage1/Program.cs | 17 ++ .../UploadIndexStage1.csproj | 4 + .../Extensions.cs | 127 +++++++++++++++ .../source-indexer.ServiceDefaults.csproj | 22 +++ 8 files changed, 250 insertions(+), 162 deletions(-) delete mode 100644 src/SourceBrowser/src/SourceIndexServer/Startup.cs create mode 100644 src/source-indexer.ServiceDefaults/Extensions.cs create mode 100644 src/source-indexer.ServiceDefaults/source-indexer.ServiceDefaults.csproj diff --git a/Directory.Packages.props b/Directory.Packages.props index 3042c0c..0849208 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -2,5 +2,12 @@ + + + + + + + \ No newline at end of file diff --git a/src/SourceBrowser/src/SourceIndexServer/Program.cs b/src/SourceBrowser/src/SourceIndexServer/Program.cs index 97afa57..4ccdaaa 100644 --- a/src/SourceBrowser/src/SourceIndexServer/Program.cs +++ b/src/SourceBrowser/src/SourceIndexServer/Program.cs @@ -1,30 +1,85 @@ -using Microsoft.AspNetCore.Hosting; +using System.IO; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.FileProviders.Physical; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Microsoft.SourceBrowser.SourceIndexServer.Models; namespace Microsoft.SourceBrowser.SourceIndexServer { public class Program { + public static ILogger Logger { get; set; } + public static void Main(string[] args) { - BuildWebHost(args).Run(); - } + var builder = WebApplication.CreateBuilder(args); - public static ILogger Logger { get; set; } + builder.AddServiceDefaults(); + + builder.Logging.ClearProviders(); + builder.Logging.AddConsole(); + builder.Logging.AddAzureWebAppDiagnostics(); + + builder.Host.UseWindowsService(); + + var rootPath = Path.Combine(builder.Environment.ContentRootPath, "index"); + var subfolder = Path.Combine(rootPath, "index"); + if (File.Exists(Path.Combine(subfolder, "Projects.txt"))) + { + rootPath = subfolder; + } + + builder.Services.AddSingleton(new Index(rootPath)); + builder.Services.AddControllersWithViews(); + builder.Services.AddRazorPages(); + + var app = builder.Build(); - public static IHost BuildWebHost(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults( - builder => { builder - .UseStartup(); }) - .ConfigureLogging(logging => - { - logging.ClearProviders(); - logging.AddConsole(); - logging.AddAzureWebAppDiagnostics(); - }) - .UseWindowsService() - .Build(); + app.MapDefaultEndpoints(); + + app.UseForwardedHeaders(new ForwardedHeadersOptions + { + ForwardedHeaders = ForwardedHeaders.XForwardedFor | + ForwardedHeaders.XForwardedProto | + ForwardedHeaders.XForwardedHost, + KnownNetworks = { }, + KnownProxies = { } + }); + + app.Use(async (context, next) => + { + context.Response.Headers["X-UA-Compatible"] = "IE=edge"; + await next(); + }); + + if (app.Environment.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.Use(Helpers.ServeProxiedIndex); + + app.UseDefaultFiles(); + if (Directory.Exists(rootPath)) + { + app.UseStaticFiles(new StaticFileOptions + { + FileProvider = new PhysicalFileProvider(rootPath, ExclusionFilters.Sensitive & ~ExclusionFilters.DotPrefixed), + }); + } + app.UseStaticFiles(); + app.UseRouting(); + + app.MapRazorPages(); + app.MapControllers(); + + Logger = app.Services.GetService>(); + + app.Run(); + } } } diff --git a/src/SourceBrowser/src/SourceIndexServer/SourceIndexServer.csproj b/src/SourceBrowser/src/SourceIndexServer/SourceIndexServer.csproj index d2fe663..3c1b620 100644 --- a/src/SourceBrowser/src/SourceIndexServer/SourceIndexServer.csproj +++ b/src/SourceBrowser/src/SourceIndexServer/SourceIndexServer.csproj @@ -16,6 +16,7 @@ + diff --git a/src/SourceBrowser/src/SourceIndexServer/Startup.cs b/src/SourceBrowser/src/SourceIndexServer/Startup.cs deleted file mode 100644 index 782b8af..0000000 --- a/src/SourceBrowser/src/SourceIndexServer/Startup.cs +++ /dev/null @@ -1,145 +0,0 @@ -using System.Collections; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.HttpOverrides; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.FileProviders; -using Microsoft.Extensions.FileProviders.Physical; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Microsoft.SourceBrowser.SourceIndexServer.Models; - -namespace Microsoft.SourceBrowser.SourceIndexServer -{ - public class Startup - { - public Startup(IWebHostEnvironment env) - { - Environment = env; - } - - public IWebHostEnvironment Environment { get; } - - // This method gets called by the runtime. Use this method to add services to the container. - // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 - public void ConfigureServices(IServiceCollection services) - { - RootPath = Path.Combine(Environment.ContentRootPath, "index"); - - var subfolder = Path.Combine(RootPath, "index"); - if (File.Exists(Path.Combine(subfolder, "Projects.txt"))) - { - RootPath = subfolder; - } - - services.AddSingleton(new Index(RootPath)); - services.AddControllersWithViews(); - services.AddRazorPages(); - - // Add health checks - //services.AddHealthChecks() - //.AddCheck( - //name: "storage", - //tags: ["ready"]) - //.AddCheck( - //name: "startup", - //check: () => HealthCheckResult.Healthy("Application is running"), - //tags: ["alive"]); - } - - public string RootPath { get; set; } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - // Configure forwarded headers for Azure Front Door - app.UseForwardedHeaders(new ForwardedHeadersOptions - { - ForwardedHeaders = ForwardedHeaders.XForwardedFor | - ForwardedHeaders.XForwardedProto | - ForwardedHeaders.XForwardedHost, - KnownNetworks = { }, - KnownProxies = { } - }); - - app.Use(async (context, next) => - { - context.Response.Headers["X-UA-Compatible"] = "IE=edge"; - await next(); - }); - - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.Use(Helpers.ServeProxiedIndex); - - app.UseDefaultFiles(); - if (Directory.Exists(RootPath)) - { - app.UseStaticFiles(new StaticFileOptions - { - FileProvider = new PhysicalFileProvider(RootPath, ExclusionFilters.Sensitive & ~ExclusionFilters.DotPrefixed), - }); - } - app.UseStaticFiles(); - app.UseRouting(); - - app.UseEndpoints(endPoints => - { - //const int healthCacheSeconds = 30; - - //static Task CacheableMinimalResponse(HttpContext context, HealthReport report) - //{ - //context.Response.Headers.CacheControl = $"public,max-age={healthCacheSeconds}"; - //context.Response.Headers.Pragma = "public"; - //context.Response.Headers.Expires = "0"; - //return HealthChecks.HealthCheckResponseWriter.WriteMinimalResponse(context, report); - //} - - //// Health check endpoints - //// Basic health check with minimal information (cached by default) - //endPoints.MapHealthChecks("/health", new HealthCheckOptions - //{ - //Predicate = _ => true, - //ResponseWriter = CacheableMinimalResponse - //}); - - //// Liveness probe (always healthy if app is running) - //endPoints.MapHealthChecks("/health/alive", new HealthCheckOptions - //{ - //Predicate = check => check.Tags.Contains("alive"), - //ResponseWriter = HealthChecks.HealthCheckResponseWriter.WriteMinimalResponse - //}); - - //if (env.IsDevelopment() || Helpers.DebugLoggingEnabled) - //{ - //// Detailed health check with full diagnostics - //endPoints.MapHealthChecks("/health/detailed", new HealthCheckOptions - //{ - //Predicate = _ => true, - //ResponseWriter = HealthChecks.HealthCheckResponseWriter.WriteResponse - //}); - - //// Readiness probe (checks storage) - //endPoints.MapHealthChecks("/health/ready", new HealthCheckOptions - //{ - //Predicate = check => check.Tags.Contains("ready"), - //ResponseWriter = HealthChecks.HealthCheckResponseWriter.WriteMinimalResponse - //}); - //} - - endPoints.MapRazorPages(); - endPoints.MapControllers(); - }); - - // Retrieve and store the logger - Program.Logger = app.ApplicationServices.GetService>(); - } - } -} diff --git a/src/UploadIndexStage1/Program.cs b/src/UploadIndexStage1/Program.cs index 3ed2445..6b0dfab 100644 --- a/src/UploadIndexStage1/Program.cs +++ b/src/UploadIndexStage1/Program.cs @@ -12,6 +12,7 @@ using Azure.Storage.Blobs.Models; using ICSharpCode.SharpZipLib.GZip; using ICSharpCode.SharpZipLib.Tar; +using Microsoft.Extensions.Hosting; using Mono.Options; namespace UploadIndexStage1 @@ -19,6 +20,22 @@ namespace UploadIndexStage1 class Program { static async Task Main(string[] args) + { + var hostBuilder = Host.CreateApplicationBuilder(args); + hostBuilder.AddServiceDefaults(); + using var host = hostBuilder.Build(); + await host.StartAsync(); + try + { + await RunAsync(args); + } + finally + { + await host.StopAsync(); + } + } + + static async Task RunAsync(string[] args) { string sourceFolder = null; string repoName = null; diff --git a/src/UploadIndexStage1/UploadIndexStage1.csproj b/src/UploadIndexStage1/UploadIndexStage1.csproj index 1af973c..739b06b 100644 --- a/src/UploadIndexStage1/UploadIndexStage1.csproj +++ b/src/UploadIndexStage1/UploadIndexStage1.csproj @@ -16,4 +16,8 @@ + + + + diff --git a/src/source-indexer.ServiceDefaults/Extensions.cs b/src/source-indexer.ServiceDefaults/Extensions.cs new file mode 100644 index 0000000..4fdcccf --- /dev/null +++ b/src/source-indexer.ServiceDefaults/Extensions.cs @@ -0,0 +1,127 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ServiceDiscovery; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace Microsoft.Extensions.Hosting; + +// Adds common Aspire services: service discovery, resilience, health checks, and OpenTelemetry. +// This project should be referenced by each service project in your solution. +// To learn more about using this project, see https://aka.ms/aspire/service-defaults +public static class Extensions +{ + private const string HealthEndpointPath = "/health"; + private const string AlivenessEndpointPath = "/alive"; + + public static TBuilder AddServiceDefaults(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.ConfigureOpenTelemetry(); + + builder.AddDefaultHealthChecks(); + + builder.Services.AddServiceDiscovery(); + + builder.Services.ConfigureHttpClientDefaults(http => + { + // Turn on resilience by default + http.AddStandardResilienceHandler(); + + // Turn on service discovery by default + http.AddServiceDiscovery(); + }); + + // Uncomment the following to restrict the allowed schemes for service discovery. + // builder.Services.Configure(options => + // { + // options.AllowedSchemes = ["https"]; + // }); + + return builder; + } + + public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + }); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); + }) + .WithTracing(tracing => + { + tracing.AddSource(builder.Environment.ApplicationName) + .AddAspNetCoreInstrumentation(tracing => + // Exclude health check requests from tracing + tracing.Filter = context => + !context.Request.Path.StartsWithSegments(HealthEndpointPath) + && !context.Request.Path.StartsWithSegments(AlivenessEndpointPath) + ) + // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) + //.AddGrpcClientInstrumentation() + .AddHttpClientInstrumentation(); + }); + + builder.AddOpenTelemetryExporters(); + + return builder; + } + + private static TBuilder AddOpenTelemetryExporters(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); + + if (useOtlpExporter) + { + builder.Services.AddOpenTelemetry().UseOtlpExporter(); + } + + // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) + //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) + //{ + // builder.Services.AddOpenTelemetry() + // .UseAzureMonitor(); + //} + + return builder; + } + + public static TBuilder AddDefaultHealthChecks(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Services.AddHealthChecks() + // Add a default liveness check to ensure app is responsive + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + + return builder; + } + + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + // Adding health checks endpoints to applications in non-development environments has security implications. + // See https://aka.ms/aspire/healthchecks for details before enabling these endpoints in non-development environments. + if (app.Environment.IsDevelopment()) + { + // All health checks must pass for app to be considered ready to accept traffic after starting + app.MapHealthChecks(HealthEndpointPath); + + // Only health checks tagged with the "live" tag must pass for app to be considered alive + app.MapHealthChecks(AlivenessEndpointPath, new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + } + + return app; + } +} diff --git a/src/source-indexer.ServiceDefaults/source-indexer.ServiceDefaults.csproj b/src/source-indexer.ServiceDefaults/source-indexer.ServiceDefaults.csproj new file mode 100644 index 0000000..70fa25c --- /dev/null +++ b/src/source-indexer.ServiceDefaults/source-indexer.ServiceDefaults.csproj @@ -0,0 +1,22 @@ + + + + net9.0;net10.0 + enable + enable + true + + + + + + + + + + + + + + + From afd09a5835f2a71ca8ffcc724b8d95ea63b9eb04 Mon Sep 17 00:00:00 2001 From: Jose Perez Rodriguez Date: Fri, 15 May 2026 22:43:18 -0700 Subject: [PATCH 4/7] Address PR feedback: simplify HtmlGenerator ref, drop net10 from ServiceDefaults Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/source-indexer.AppHost/source-indexer.AppHost.csproj | 7 +------ .../source-indexer.ServiceDefaults.csproj | 2 +- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/source-indexer.AppHost/source-indexer.AppHost.csproj b/src/source-indexer.AppHost/source-indexer.AppHost.csproj index e294598..e30db68 100644 --- a/src/source-indexer.AppHost/source-indexer.AppHost.csproj +++ b/src/source-indexer.AppHost/source-indexer.AppHost.csproj @@ -21,12 +21,7 @@ - - + diff --git a/src/source-indexer.ServiceDefaults/source-indexer.ServiceDefaults.csproj b/src/source-indexer.ServiceDefaults/source-indexer.ServiceDefaults.csproj index 70fa25c..09110f1 100644 --- a/src/source-indexer.ServiceDefaults/source-indexer.ServiceDefaults.csproj +++ b/src/source-indexer.ServiceDefaults/source-indexer.ServiceDefaults.csproj @@ -1,7 +1,7 @@ - net9.0;net10.0 + net9.0 enable enable true From 15013961fa79649aac2edd7cc4d83f5975623e17 Mon Sep 17 00:00:00 2001 From: Jose Perez Rodriguez Date: Sat, 16 May 2026 08:03:18 -0700 Subject: [PATCH 5/7] docs: link inner-loop guide from README and add debugging section Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 21 ++++++++++++++++----- docs/inner-loop.md | 18 ++++++++++++------ 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index e64b2d7..19ed0dd 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ This repo contains the code for building http://source.dot.net ## Documentation +- [Local inner loop (Aspire)](docs/inner-loop.md) - **Recommended for development.** Runs the full pipeline (sample build → BinLogToSln → HtmlGenerator → blob → SourceIndexServer) locally against a tiny sample library. - [Source Selection Algorithm](docs/source-selection-algorithm.md) - How the indexer chooses the best implementation when multiple builds exist for the same assembly ## Build Status @@ -10,17 +11,27 @@ This repo contains the code for building http://source.dot.net ## What Is It? This repo uses https://github.com/KirillOsenkov/SourceBrowser (with a few additions here https://github.com/dotnet/SourceBrowser/tree/source-indexer) to index the dotnet sources and produce a navigatable and searchable website containing the full source code. This includes code from the runtime, winforms, wpf, aspnetcore, and msbuild, among others. For a full list see here https://github.com/dotnet/source-indexer/blob/main/src/index/repositories.props. -## Build Prerequsites -The build requires .NET 8.0 and Visual Studio 2022 to build. +## Local development (Aspire) +The repository ships with an [Aspire](https://aspire.dev) AppHost (`src/source-indexer.AppHost/`) that emulates the full production pipeline locally against a tiny sample library — **this is the recommended way to work on the repo**. + +```pwsh +aspire start +``` + +The dashboard URL (with login token) is printed to the console. From there you can run the `bootstrap-all` step to build the sample, run `BinLogToSln`, run `HtmlGenerator`, upload to Azurite, and start the web app. See [docs/inner-loop.md](docs/inner-loop.md) for the full resource graph and walkthroughs. + +You can also open the repo in **Visual Studio** or **VS Code** to set breakpoints and debug individual components (e.g., `HtmlGenerator`, `SourceIndexServer`, `BinLogToSln`) while the rest of the pipeline runs under the AppHost. + +## Building the production index (Windows-only) +The official pipeline build only works on Windows because `HtmlGenerator` is a .NET Framework executable. For local dev prefer the Aspire flow above; use this path only if you need to reproduce the exact pipeline build. + +**Prerequisites:** .NET 8.0 and Visual Studio 2022. -## Build -The build will only work on windows because the source indexer executable is a .net framework executable. 1. `git clone https://github.com/dotnet/source-indexer.git` 2. For each *.sln file `dotnet restore` 3. Find VS 2022 msbuild.exe on your machine, typically found at `C:\Program Files (x86)\Microsoft Visual Studio\2022\Enterprise\MSBuild\Current\Bin\MSBuild.exe` 4. `msbuild build.proj` -## Running the built index After the build is finished the index will exist in `bin\index` and can be run by running `dotnet Microsoft.SourceBrowser.SourceIndexServer.dll` in that folder. The index will be served on `http://localhost:5000` ## Deployment diff --git a/docs/inner-loop.md b/docs/inner-loop.md index 656111d..4a53aec 100644 --- a/docs/inner-loop.md +++ b/docs/inner-loop.md @@ -92,11 +92,17 @@ root. Blobs uploaded during a session survive `aspire start` restarts and container recreations — to wipe state, just delete the `.azurite/` folder. The folder is gitignored. +## Debugging individual components + +The AppHost runs each stage as its own resource, but the underlying projects are normal .NET projects you can debug directly in **Visual Studio** or **VS Code**: + +- **`HtmlGenerator`** (`src/SourceBrowser/src/HtmlGenerator/`) — set breakpoints, then either start it from the IDE pointing at an existing binlog under `samples/MiniRuntime/bin/sample/`, or attach to the `step3-htmlgenerator` process after kicking it off from the dashboard. +- **`SourceIndexServer`** (`src/SourceBrowser/src/SourceIndexServer/`) — F5 from the IDE for fast inner-loop on the web UI. To debug against Azurite data, set `AZURE_STORAGE_CONNECTION_STRING` and `SOURCE_BROWSER_INDEX_PROXY_URL` from the running AppHost (visible on the `web` resource's env vars panel in the dashboard) in your launch profile. +- **`UploadIndexStage1`** (`src/UploadIndexStage1/`) — same pattern: copy the env vars off the AppHost's `step2-upload-stage1` resource and run from the IDE. +- **`BinLogToSln`** (`src/SourceBrowser/src/BinLogToSln/`) — runnable directly against any `.binlog` produced by `step1-sample-build`. + +The Aspire dashboard's per-resource "Environment" tab is the source of truth for the env vars Aspire injects — copy those into your `launchSettings.json` to reproduce the AppHost environment under a debugger. + ## Open follow-ups -- **ServiceDefaults / dashboard telemetry**: `SourceIndexServer` still uses - the legacy `IHostBuilder` + `Startup` bootstrap. Wiring - `AddServiceDefaults()` / `MapDefaultEndpoints()` requires migrating to - the minimal-hosting model first. Until then, the `web` resource won't - contribute traces/metrics to the Aspire dashboard, but everything else - works. +- **End-to-end OTel validation**: ServiceDefaults are wired into `SourceIndexServer` and `UploadIndexStage1`, but flowing traces across `HtmlGenerator` (net472) into the dashboard hasn't been validated yet. From 6bff7db2603d40e6aa69d9495ae733c487c6124f Mon Sep 17 00:00:00 2001 From: Jose Perez Rodriguez Date: Sat, 16 May 2026 08:21:27 -0700 Subject: [PATCH 6/7] docs: add bootstrap pipeline screenshot to inner-loop guide Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/images/bootstrap-menu.png | Bin 0 -> 86436 bytes docs/inner-loop.md | 14 +++++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) create mode 100644 docs/images/bootstrap-menu.png diff --git a/docs/images/bootstrap-menu.png b/docs/images/bootstrap-menu.png new file mode 100644 index 0000000000000000000000000000000000000000..e195777031dac7d775ae7723e4f513d5c34ef5e7 GIT binary patch literal 86436 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 literal 0 HcmV?d00001 diff --git a/docs/inner-loop.md b/docs/inner-loop.md index 4a53aec..5e1b871 100644 --- a/docs/inner-loop.md +++ b/docs/inner-loop.md @@ -44,17 +44,21 @@ Explicit-start (stopped by default — click **Start** in the dashboard): ## One-click bootstrap -The `prodStorage` resource exposes a custom **bootstrap-all** command that -runs the four pipeline resources in order (`step1-sample-build` → -`step2-upload-stage1` → `step3-htmlgenerator` → `step4-publish-index`) and -waits for each one to finish. This is the easy first-run path: +The `prodStorage` resource exposes a custom **Bootstrap full pipeline** +command (internal id `bootstrap-all`) that runs the four pipeline +resources in order (`step1-sample-build` → `step2-upload-stage1` → +`step3-htmlgenerator` → `step4-publish-index`) and waits for each one to +finish. This is the easy first-run path: 1. `aspire start` 2. Open the dashboard. -3. On the `prodStorage` resource, click the **bootstrap-all** command. +3. On the `prodStorage` row, click the **⋯** button in the Actions + column and choose **Bootstrap full pipeline**. 4. Wait for it to finish (watch the logs). 5. Open the `web` URL — you should see `MiniRuntime`'s indexed HTML. +![Bootstrap full pipeline menu on prodStorage](images/bootstrap-menu.png) + Re-running any individual resource regenerates just that stage. ## How this maps to prod From ee3f4032f69ca303a25db4219cab9b8c586cdefb Mon Sep 17 00:00:00 2001 From: Jose Perez Rodriguez Date: Thu, 11 Jun 2026 10:13:20 -0700 Subject: [PATCH 7/7] Update Aspire to 13.4.3 Ran spire update to bump Aspire.AppHost.Sdk and Aspire.Hosting.Azure.Storage from 13.3.2 to 13.4.3, then migrated off the deprecated AddBlobContainer overload on AzureBlobStorageResource to the new direct-on-AzureStorageResource API. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- NuGet.config | 13 +++++++++++-- src/source-indexer.AppHost/AppHost.cs | 4 ++-- .../source-indexer.AppHost.csproj | 4 ++-- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/NuGet.config b/NuGet.config index 8ec79f9..2724eaf 100644 --- a/NuGet.config +++ b/NuGet.config @@ -1,11 +1,20 @@ - + + - + + + + + + + + + \ No newline at end of file diff --git a/src/source-indexer.AppHost/AppHost.cs b/src/source-indexer.AppHost/AppHost.cs index 30be1ef..7438345 100644 --- a/src/source-indexer.AppHost/AppHost.cs +++ b/src/source-indexer.AppHost/AppHost.cs @@ -24,7 +24,7 @@ }); var stage1Blobs = stage1Storage.AddBlobs("stage1-blobs"); -var stage1Container = stage1Blobs.AddBlobContainer("stage1", blobContainerName: "stage1"); +var stage1Container = stage1Storage.AddBlobContainer("stage1", blobContainerName: "stage1"); var prodStorage = builder.AddAzureStorage("prodStorage") .RunAsEmulator(azurite => @@ -34,7 +34,7 @@ }); var prodBlobs = prodStorage.AddBlobs("prod-blobs"); -var indexContainer = prodBlobs.AddBlobContainer("index-local", blobContainerName: "index-local"); +var indexContainer = prodStorage.AddBlobContainer("index-local", blobContainerName: "index-local"); // ============================================================================= // Pipeline resources — all WithExplicitStart() so they only run when the user diff --git a/src/source-indexer.AppHost/source-indexer.AppHost.csproj b/src/source-indexer.AppHost/source-indexer.AppHost.csproj index e30db68..eb5012e 100644 --- a/src/source-indexer.AppHost/source-indexer.AppHost.csproj +++ b/src/source-indexer.AppHost/source-indexer.AppHost.csproj @@ -1,4 +1,4 @@ - + Exe @@ -15,7 +15,7 @@ - +