feat: add opt-in publish to understand-quickly registry#361
Open
amacsmith wants to merge 2 commits intoAIDotNet:mainfrom
Open
feat: add opt-in publish to understand-quickly registry#361amacsmith wants to merge 2 commits intoAIDotNet:mainfrom
amacsmith wants to merge 2 commits intoAIDotNet:mainfrom
Conversation
OpenDeepWiki already produces a graphify-out/graph.json artifact via
GraphifyCliRunner. This PR adds a small UnderstandQuicklyPublisher
service that, after a successful run, stamps
metadata.{tool, tool_version, generated_at, commit} into the JSON and
— when UNDERSTAND_QUICKLY_TOKEN is set — fires a
repository_dispatch event at looptech-ai/understand-quickly so the
public registry of code-knowledge graphs picks up the entry.
The service is opt-in (Enabled=false by default; UnderstandQuickly:Token
config or UNDERSTAND_QUICKLY_TOKEN env var enables it). Network
failures are logged but never fail the artifact — the local graph.json
is always written first.
Three xunit tests covering the disabled, no-token, and dispatched
paths. Uses Moq.Protected for the HttpMessageHandler stub, matching
the existing GraphifyCliRunnerTests style.
Spec: https://github.com/looptech-ai/understand-quickly/blob/main/docs/spec/code-graph-protocol.md
Action: https://github.com/looptech-ai/uq-publish-action
Contributor
There was a problem hiding this comment.
Pull request overview
Adds an opt-in integration that stamps registry metadata into Graphify’s generated graph.json and (when configured with a GitHub token) triggers a repository_dispatch to the external understand-quickly registry, so published graphs can be discovered/consumed via the registry’s stable API and MCP server.
Changes:
- Introduces
UnderstandQuicklyPublisher+ options to stampmetadata.{tool, tool_version, generated_at, commit}and optionally dispatch a registry resync event. - Hooks the publisher into the Graphify artifact worker after a successful Graphify run (non-blocking, best-effort).
- Adds xUnit tests for disabled/enabled/no-token/token-dispatch cases, and documents
UNDERSTAND_QUICKLY_TOKENin the README.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Documents new UNDERSTAND_QUICKLY_TOKEN env var and intended behavior. |
| src/OpenDeepWiki/Program.cs | Adds DI/options binding for the opt-in UnderstandQuickly publisher. |
| src/OpenDeepWiki/Services/Graphify/GraphifyArtifactWorker.cs | Invokes publisher after successful Graphify generation (best-effort). |
| src/OpenDeepWiki/Services/Graphify/UnderstandQuicklyPublisher.cs | Implements metadata stamping + optional GitHub repository_dispatch call. |
| tests/OpenDeepWiki.Tests/Services/Graphify/UnderstandQuicklyPublisherTests.cs | Adds test coverage for publisher no-op/stamp/dispatch behaviors. |
Comment on lines
+49
to
+53
| HttpClient? httpClient = null) | ||
| { | ||
| _options = options.Value; | ||
| _logger = logger; | ||
| _httpClient = httpClient ?? new HttpClient(); |
Comment on lines
+114
to
+125
| var payload = new JsonObject | ||
| { | ||
| ["event_type"] = DispatchEventType, | ||
| ["client_payload"] = new JsonObject | ||
| { | ||
| ["repo"] = repoSlug, | ||
| ["schema"] = _options.Schema, | ||
| ["graph_path"] = result.GraphJsonPath, | ||
| ["tool"] = ToolName, | ||
| ["tool_version"] = toolVersion, | ||
| ["commit"] = result.CommitId, | ||
| }, |
| await using var read = File.OpenRead(result.GraphJsonPath); | ||
| var node = await JsonNode.ParseAsync(read, cancellationToken: cancellationToken) | ||
| ?? new JsonObject(); | ||
| await read.DisposeAsync(); |
Comment on lines
+96
to
+100
| catch (Exception ex) | ||
| { | ||
| _logger.LogWarning(ex, "Failed to stamp metadata into {Path}", result.GraphJsonPath); | ||
| return new UnderstandQuicklyPublishResult(false, false, ex.Message); | ||
| } |
Comment on lines
+29
to
+80
| Directory.CreateDirectory(tmp); | ||
| var graph = Path.Combine(tmp, "graph.json"); | ||
| await File.WriteAllTextAsync(graph, "{\"nodes\":[],\"links\":[]}"); | ||
|
|
||
| var options = Options.Create(new UnderstandQuicklyOptions { Enabled = false }); | ||
| var publisher = new UnderstandQuicklyPublisher( | ||
| options, NullLogger<UnderstandQuicklyPublisher>.Instance); | ||
|
|
||
| var result = await publisher.PublishAsync(MakeResult(graph), "owner/repo"); | ||
|
|
||
| Assert.False(result.MetadataStamped); | ||
| Assert.False(result.Dispatched); | ||
|
|
||
| // File untouched. | ||
| var doc = JsonNode.Parse(await File.ReadAllTextAsync(graph)); | ||
| Assert.Null(doc!["metadata"]); | ||
|
|
||
| Directory.Delete(tmp, recursive: true); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task PublishAsync_EnabledNoToken_StampsButDoesNotDispatch() | ||
| { | ||
| var tmp = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); | ||
| Directory.CreateDirectory(tmp); | ||
| var graph = Path.Combine(tmp, "graph.json"); | ||
| await File.WriteAllTextAsync(graph, "{\"nodes\":[],\"links\":[]}"); | ||
|
|
||
| var options = Options.Create(new UnderstandQuicklyOptions { Enabled = true }); | ||
| var handler = new Mock<HttpMessageHandler>(); | ||
| handler.Protected() | ||
| .Setup<Task<HttpResponseMessage>>("SendAsync", | ||
| ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>()) | ||
| .ThrowsAsync(new InvalidOperationException("dispatch should not be called")); | ||
| var publisher = new UnderstandQuicklyPublisher( | ||
| options, NullLogger<UnderstandQuicklyPublisher>.Instance, | ||
| new HttpClient(handler.Object)); | ||
|
|
||
| var result = await publisher.PublishAsync(MakeResult(graph, "abc123"), "owner/repo"); | ||
|
|
||
| Assert.True(result.MetadataStamped); | ||
| Assert.False(result.Dispatched); | ||
| Assert.Null(result.Error); | ||
|
|
||
| var doc = JsonNode.Parse(await File.ReadAllTextAsync(graph))!.AsObject(); | ||
| var md = doc["metadata"]!.AsObject(); | ||
| Assert.Equal("opendeepwiki", md["tool"]!.GetValue<string>()); | ||
| Assert.Equal("abc123", md["commit"]!.GetValue<string>()); | ||
| Assert.NotNull(md["tool_version"]); | ||
| Assert.EndsWith("Z", md["generated_at"]!.GetValue<string>()); | ||
|
|
||
| Directory.Delete(tmp, recursive: true); |
Comment on lines
+52
to
+81
| var tmp = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); | ||
| Directory.CreateDirectory(tmp); | ||
| var graph = Path.Combine(tmp, "graph.json"); | ||
| await File.WriteAllTextAsync(graph, "{\"nodes\":[],\"links\":[]}"); | ||
|
|
||
| var options = Options.Create(new UnderstandQuicklyOptions { Enabled = true }); | ||
| var handler = new Mock<HttpMessageHandler>(); | ||
| handler.Protected() | ||
| .Setup<Task<HttpResponseMessage>>("SendAsync", | ||
| ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>()) | ||
| .ThrowsAsync(new InvalidOperationException("dispatch should not be called")); | ||
| var publisher = new UnderstandQuicklyPublisher( | ||
| options, NullLogger<UnderstandQuicklyPublisher>.Instance, | ||
| new HttpClient(handler.Object)); | ||
|
|
||
| var result = await publisher.PublishAsync(MakeResult(graph, "abc123"), "owner/repo"); | ||
|
|
||
| Assert.True(result.MetadataStamped); | ||
| Assert.False(result.Dispatched); | ||
| Assert.Null(result.Error); | ||
|
|
||
| var doc = JsonNode.Parse(await File.ReadAllTextAsync(graph))!.AsObject(); | ||
| var md = doc["metadata"]!.AsObject(); | ||
| Assert.Equal("opendeepwiki", md["tool"]!.GetValue<string>()); | ||
| Assert.Equal("abc123", md["commit"]!.GetValue<string>()); | ||
| Assert.NotNull(md["tool_version"]); | ||
| Assert.EndsWith("Z", md["generated_at"]!.GetValue<string>()); | ||
|
|
||
| Directory.Delete(tmp, recursive: true); | ||
| } |
Comment on lines
+86
to
+123
| var tmp = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); | ||
| Directory.CreateDirectory(tmp); | ||
| var graph = Path.Combine(tmp, "graph.json"); | ||
| await File.WriteAllTextAsync(graph, "{\"nodes\":[],\"links\":[]}"); | ||
|
|
||
| HttpRequestMessage? captured = null; | ||
| var handler = new Mock<HttpMessageHandler>(); | ||
| handler.Protected() | ||
| .Setup<Task<HttpResponseMessage>>("SendAsync", | ||
| ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>()) | ||
| .Callback<HttpRequestMessage, CancellationToken>((req, _) => captured = req) | ||
| .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.NoContent)); | ||
|
|
||
| var options = Options.Create(new UnderstandQuicklyOptions | ||
| { | ||
| Enabled = true, | ||
| Token = "ghp_fake", | ||
| }); | ||
| var publisher = new UnderstandQuicklyPublisher( | ||
| options, NullLogger<UnderstandQuicklyPublisher>.Instance, | ||
| new HttpClient(handler.Object)); | ||
|
|
||
| var result = await publisher.PublishAsync(MakeResult(graph), "looptech-ai/demo"); | ||
|
|
||
| Assert.True(result.MetadataStamped); | ||
| Assert.True(result.Dispatched); | ||
| Assert.NotNull(captured); | ||
| Assert.Equal("https://api.github.com/repos/looptech-ai/understand-quickly/dispatches", | ||
| captured!.RequestUri!.ToString()); | ||
| Assert.Equal("Bearer", captured.Headers.Authorization!.Scheme); | ||
| var body = await captured.Content!.ReadAsStringAsync(); | ||
| var payload = JsonDocument.Parse(body).RootElement; | ||
| Assert.Equal("uq-publish", payload.GetProperty("event_type").GetString()); | ||
| Assert.Equal("looptech-ai/demo", | ||
| payload.GetProperty("client_payload").GetProperty("repo").GetString()); | ||
|
|
||
| Directory.Delete(tmp, recursive: true); | ||
| } |
| options.Enabled = true; | ||
| } | ||
| }); | ||
| builder.Services.AddScoped<IUnderstandQuicklyPublisher, UnderstandQuicklyPublisher>(); |
Comment on lines
+141
to
+156
| if (response.IsSuccessStatusCode) | ||
| { | ||
| _logger.LogInformation( | ||
| "UnderstandQuickly: dispatched to {Registry} for {Slug} (HTTP {Code}).", | ||
| _options.RegistryRepo, repoSlug, (int)response.StatusCode); | ||
| return new UnderstandQuicklyPublishResult(true, true); | ||
| } | ||
| if (response.StatusCode == System.Net.HttpStatusCode.NotFound) | ||
| { | ||
| _logger.LogInformation( | ||
| "UnderstandQuickly: {Slug} not in registry — register once with `npx @understand-quickly/cli add`.", | ||
| repoSlug); | ||
| return new UnderstandQuicklyPublishResult(true, false, "repo not registered"); | ||
| } | ||
| return new UnderstandQuicklyPublishResult(true, false, $"HTTP {(int)response.StatusCode}"); | ||
| } |
Address Copilot review on PR AIDotNet#361: - Register `UnderstandQuicklyPublisher` via `AddHttpClient<,>` (typed client) so the framework manages `HttpMessageHandler` lifetimes. The constructor now takes a non-nullable `HttpClient`; drop the `httpClient ?? new HttpClient()` fallback that leaked undisposed handlers in a scoped service. - Send `graph_path` as a repo-relative, forward-slash path derived from `OutputRoot` instead of the absolute server path. The understand-quickly registry fetches via `raw.githubusercontent.com` and an absolute path was both incorrect and a minor info leak in Action logs. Falls back to the file name when OutputRoot is missing. - Let `OperationCanceledException` propagate out of both broad catch blocks so the background worker can shut down cleanly instead of converting cancellation into a misleading warning. - Drop the redundant explicit `await read.DisposeAsync()` after the `await using` declaration; switch to a block-scoped `await using` so the read stream is disposed before the write begins. - Log status code + (trimmed) response body on non-success / non-404 responses to make 401/403/422 triage practical. - Wrap test temp-dir cleanup in try/finally so a failed assertion doesn't leak directories on the CI agent. Add coverage asserting `graph_path` is repo-relative, plus a `ToRepoRelativePath` unit test using platform-real paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Author
|
Thanks for the Copilot review — addressed the substantive findings in 1751236:
I couldn't run |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
looptech-ai/understand-quicklyis a public registry of code-knowledge graphs that ships an MCP server and a stableregistry.jsonAPI. OpenDeepWiki already runsgraphifyand produces agraphify-out/graph.jsonper repo viaGraphifyCliRunner. That artifact is exactly the producer shape the registry is designed for — wiring an opt-in publish step means every OpenDeepWiki deployment can land its generated graphs in the registry, and AI agents (Claude, Codex, Cursor via MCP) can discover and consume them immediately without each user pointing at a private OpenDeepWiki endpoint.raw.githubusercontent.com.metadata.commitis set), and a turnkey MCP server.What changes
UnderstandQuicklyPublisherservice inServices/Graphify/that, given aGraphifyRunResult:metadata.{tool, tool_version, generated_at, commit}into thegraph.jsonproduced byGraphifyCliRunner.repository_dispatchevent atlooptech-ai/understand-quicklyso the registry's sync workflow re-pulls the entry.GraphifyArtifactWorker.ProcessArtifactAsyncthat calls the publisher after a successful run. Failures are logged but never fail the artifact — the localgraph.jsonis always written first.Program.cswith both anappsettings.jsonUnderstandQuickly:section and anUNDERSTAND_QUICKLY_TOKENenv-var override (mirrors the existingGRAPHIFY_*env-var pattern).GraphifyCliRunnerTestsstyle.Diff stats
The publisher itself is 163 LoC; the rest is tests + docs + DI wiring.
No-op default
The service is opt-in:
UnderstandQuickly:Enableddefaults tofalse.(MetadataStamped: false, Dispatched: false)immediately when disabled — no metadata mutation, no network call.Tokenonly stamps the local file (no dispatch).runner.GenerateAsyncsucceeds andartifact.Status = Completedis recorded. Failures inside the publisher are caught and logged.Token setup
Fine-grained GitHub PAT, single permission:
looptech-ai/understand-quicklyonly.Repository dispatches: write. Nothing else.Set via
appsettings.json:{ "UnderstandQuickly": { "Enabled": true, "Token": "ghp_..." } }…or via environment:
For environments running OpenDeepWiki in CI, the
looptech-ai/uq-publish-action@v0.1.0Marketplace Action is available as the source-side fallback (collapses the dispatch step to ~5 lines of YAML).Test plan
UnderstandQuicklyPublisherTests.PublishAsync_Disabled_NoOpAndNoStamp— disabled service does not touch the file or call HTTP.UnderstandQuicklyPublisherTests.PublishAsync_EnabledNoToken_StampsButDoesNotDispatch— metadata is stamped; HTTP is not called (verified via Moq throwing if invoked).UnderstandQuicklyPublisherTests.PublishAsync_EnabledWithToken_DispatchesAndReportsSuccess— verifies request URL, Bearer auth, payload structure, and theevent_type=uq-publishshape.UNDERSTAND_QUICKLY_TOKEN=… dotnet test— verify the new tests pass in your CI environment (we don't have .NET 10 available in our test sandbox).sync.ymlruns within ~1 minute.Schema fit
graphify-out/graph.json(innode_link_datashape withnodesandlinksarrays) maps cleanly to the registry'sgitnexus@1schema. Drift detection works as long asmetadata.commitis populated — which the publisher sets fromresult.CommitId(already populated byGraphifyCliRunner).Notes
AIDotNet/OpenDeepWikito the verified-publisher allowlist for auto-merge of registry updates.--publishis governed by the Understand-Quickly Data License 1.0. It is opt-in, gated on the user explicitly setting the token.Links
gitnexus@1schema: https://github.com/looptech-ai/understand-quickly/blob/main/schemas/gitnexus@1.jsonCurrent live state (sweep 2026-05-11)
The registry and its publishing surface are tagged and live:
looptech-ai/understand-quicklyv0.3.0@looptech-ai/understand-quickly-cli@0.1.2on npm@looptech-ai/understand-quickly-mcp@0.1.2on npmunderstand-quickly0.1.1 on PyPIlooptech-ai/uq-publish-action@v0.1.0on the GitHub Marketplaceio.github.looptech-ai/understand-quicklyNothing in this PR depends on any pre-release surface — all referenced artifacts are pinned versions.