diff options
| author | Leitwolf11 | 2026-10-15 18:35:17 +0300 |
|---|---|---|
| committer | Leitwolf11 | 2026-10-15 18:35:17 +0300 |
| commit | 018696d64cdb4088c0dd502ceab8d8ed9e11543e (patch) | |
| tree | 402412587d273cd488b941878f902d313b47fa41 | |
| parent | a460c4c7e922f37705b2be3f1fa7c789f514cd51 (diff) | |
| download | website-018696d64cdb4088c0dd502ceab8d8ed9e11543e.tar.gz | |
Gate the API documentation with DocFX
The Kotlin backend checked its documentation with Dokka; the ASP.NET Core
API had only the compiler warning for a public declaration without a
comment. CI now builds the API reference with DocFX, every warning as an
error, and Documents/api/CheckDocumentation.cs fails when a namespace,
type or member of the reference has no description, internal and
private members included. The setup is the one Progmaweb uses: the
docfx tool pinned in .config/dotnet-tools.json, the configuration and
the namespace description under Documents/api, and the generated
reference ignored.
| -rw-r--r-- | .config/dotnet-tools.json | 11 | ||||
| -rw-r--r-- | .github/workflows/ci.yml | 6 | ||||
| -rw-r--r-- | .gitignore | 2 | ||||
| -rw-r--r-- | Documents/DEVELOPMENT.md | 6 | ||||
| -rw-r--r-- | Documents/api/CheckDocumentation.cs | 118 | ||||
| -rw-r--r-- | Documents/api/docfx.json | 39 | ||||
| -rw-r--r-- | Documents/api/index.md | 31 | ||||
| -rw-r--r-- | Documents/api/namespaces/Progmasoft.Tala.Website.Api.md | 10 | ||||
| -rw-r--r-- | Documents/api/packages.lock.json | 35 | ||||
| -rw-r--r-- | Documents/api/toc.yml | 7 | ||||
| -rw-r--r-- | README.md | 3 | ||||
| -rw-r--r-- | REUSE.toml | 2 |
12 files changed, 268 insertions, 2 deletions
diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 0000000..e4d2756 --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,11 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "docfx": { + "version": "2.81.0", + "commands": ["docfx"], + "rollForward": false + } + } +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4929d81..6aee49a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,6 +65,12 @@ jobs: - run: dotnet build Progmasoft.Tala.Website.slnx --configuration Release --no-restore - run: dotnet test --project backend/Progmasoft.Tala.Website.Api.Tests/Progmasoft.Tala.Website.Api.Tests.csproj --configuration Release --no-build --coverlet --coverlet-output-format cobertura --results-directory TestResults - run: dotnet publish backend/Progmasoft.Tala.Website.Api/Progmasoft.Tala.Website.Api.csproj --configuration Release --no-restore --output publish + # Documentation gate. DocFX builds the API reference with every warning + # as an error; the coverage check then fails when a namespace, type or + # member of the reference has no description. + - run: dotnet tool restore + - run: dotnet docfx Documents/api/docfx.json --warningsAsErrors + - run: dotnet run Documents/api/CheckDocumentation.cs -- Documents/api - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: website-backend-coverage @@ -17,3 +17,5 @@ build/ *.user TestResults/ publish/ +Documents/api/_site/ +Documents/api/reference/ diff --git a/Documents/DEVELOPMENT.md b/Documents/DEVELOPMENT.md index 835ae05..0bf7f55 100644 --- a/Documents/DEVELOPMENT.md +++ b/Documents/DEVELOPMENT.md @@ -41,6 +41,10 @@ dotnet run --project backend/Progmasoft.Tala.Website.Api # serves on 127.0. ``` - The build treats warnings as errors and requires a documentation comment on every public declaration. +- DocFX builds the API reference from those comments. `dotnet tool restore`, then + `dotnet docfx Documents/api/docfx.json --warningsAsErrors` and + `dotnet run Documents/api/CheckDocumentation.cs -- Documents/api`; the second fails on any namespace, type or + member of the reference without a description. See [the API reference](api/index.md). - The tests run the endpoints in an in-memory host with xUnit v3 on Microsoft.Testing.Platform. - `PORT` changes the port; the service binds to loopback only. @@ -87,7 +91,7 @@ narrow window, and keyboard focus. | Job | Enforces | | --- | --- | | `frontend` | `npm ci`, `npm audit` at moderate severity, type check, tests with coverage, format check, build | -| `backend` | locked restore, build with warnings as errors, tests with coverage, and the published application | +| `backend` | locked restore, build with warnings as errors, tests with coverage, the published application, and the DocFX documentation gate | | `frontend-coverage`, `backend-coverage` | upload coverage to Codecov | | `dependency-review` | reviews dependency changes of a pull request | | CodeQL | static analysis of the workflows, TypeScript and C# | diff --git a/Documents/api/CheckDocumentation.cs b/Documents/api/CheckDocumentation.cs new file mode 100644 index 0000000..f6b2da8 --- /dev/null +++ b/Documents/api/CheckDocumentation.cs @@ -0,0 +1,118 @@ +// SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> +// SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1 + +// Documentation coverage gate of the API reference. +// +// DocFX builds a reference from whatever comments exist and does not report a declaration that has none. This +// program reads the metadata DocFX generated and fails when a namespace, type or member is undocumented, so the +// reference cannot contain an entry without a description. +// +// Usage, after `dotnet docfx metadata Documents/api/docfx.json`: +// +// dotnet run Documents/api/CheckDocumentation.cs -- Documents/api +// +// The exit code is 0 when everything is documented, 1 when something is not, and 2 when the metadata is missing. + +using System.Text.RegularExpressions; + +string root = args.Length > 0 ? args[0] : "Documents/api"; +string referenceDirectory = Path.Combine(root, "reference"); +string namespaceDirectory = Path.Combine(root, "namespaces"); + +if (!Directory.Exists(referenceDirectory)) +{ + Console.Error.WriteLine($"No metadata in '{referenceDirectory}'. Run 'dotnet docfx metadata' first."); + return 2; +} + +// A namespace has no declaration that could carry a comment. Its description is an overwrite file named after it. +HashSet<string> describedNamespaces = Directory.Exists(namespaceDirectory) + ? Directory.EnumerateFiles(namespaceDirectory, "*.md") + .Where(HasDescription) + .Select(path => Path.GetFileNameWithoutExtension(path)!) + .ToHashSet(StringComparer.Ordinal) + : []; + +List<string> undocumented = []; +int checkedItems = 0; + +foreach (string file in Directory.EnumerateFiles(referenceDirectory, "*.yml").Order(StringComparer.Ordinal)) +{ + if (Path.GetFileName(file) == "toc.yml") + { + continue; + } + + // The items a file defines come first; the `references` section after them repeats names of other files. + string text = File.ReadAllText(file).ReplaceLineEndings("\n"); + int references = text.IndexOf("\nreferences:", StringComparison.Ordinal); + string definitions = references < 0 ? text : text[..references]; + + foreach (string item in ItemStart().Split(definitions).Skip(1)) + { + string uid = item[..item.IndexOf('\n')].Trim(); + string type = ItemType().Match(item) is { Success: true } match ? match.Groups[1].Value : "Item"; + checkedItems++; + + bool documented = type == "Namespace" + ? describedNamespaces.Contains(uid) + : ItemSummary().IsMatch(item); + if (!documented) + { + undocumented.Add($"{type} {uid}"); + } + } +} + +if (checkedItems == 0) +{ + Console.Error.WriteLine($"The metadata in '{referenceDirectory}' defines nothing; the gate would pass vacuously."); + return 2; +} + +if (undocumented.Count != 0) +{ + Console.Error.WriteLine($"{undocumented.Count} of {checkedItems} API items have no documentation:"); + foreach (string entry in undocumented) + { + Console.Error.WriteLine($" {entry}"); + } + + Console.Error.WriteLine("Add a <summary> to each declaration, or a file in Documents/api/namespaces for a namespace."); + return 1; +} + +Console.WriteLine($"All {checkedItems} API items are documented."); +return 0; + +// An overwrite file describes its namespace when it has text after its front matter and comment header. +static bool HasDescription(string path) +{ + string text = File.ReadAllText(path).ReplaceLineEndings("\n"); + int frontMatterEnd = text.IndexOf("\n---", 3, StringComparison.Ordinal); + if (!text.StartsWith("---\n", StringComparison.Ordinal) || frontMatterEnd < 0) + { + return false; + } + + string body = HtmlComment().Replace(text[(frontMatterEnd + 4)..], string.Empty); + return text[..frontMatterEnd].Contains($"uid: {Path.GetFileNameWithoutExtension(path)}", StringComparison.Ordinal) + && !string.IsNullOrWhiteSpace(body); +} + +internal static partial class Program +{ + // Every defined item starts with its identifier at the left margin. + [GeneratedRegex(@"^- uid: ", RegexOptions.Multiline)] + private static partial Regex ItemStart(); + + [GeneratedRegex(@"^ type: (\w+)", RegexOptions.Multiline)] + private static partial Regex ItemType(); + + // A summary with content: text on the same line, or a block scalar followed by an indented line. + [GeneratedRegex(@"^ summary: (?:[^\s>|].*|[>|][-+]?\n \S)", RegexOptions.Multiline)] + private static partial Regex ItemSummary(); + + [GeneratedRegex(@"<!--.*?-->", RegexOptions.Singleline)] + private static partial Regex HtmlComment(); +} diff --git a/Documents/api/docfx.json b/Documents/api/docfx.json new file mode 100644 index 0000000..58bc6c9 --- /dev/null +++ b/Documents/api/docfx.json @@ -0,0 +1,39 @@ +{ + "metadata": [ + { + "src": [ + { + "src": "../../backend/Progmasoft.Tala.Website.Api", + "files": [ + "Progmasoft.Tala.Website.Api.csproj" + ] + } + ], + "dest": "reference", + "includePrivateMembers": true + } + ], + "build": { + "content": [ + { + "files": [ + "index.md", + "toc.yml", + "reference/**/*.yml" + ] + } + ], + "dest": "_site", + "globalMetadata": { + "_appTitle": "Tala website API", + "_enableSearch": false + }, + "overwrite": [ + { + "files": [ + "namespaces/*.md" + ] + } + ] + } +} diff --git a/Documents/api/index.md b/Documents/api/index.md new file mode 100644 index 0000000..8cbed8a --- /dev/null +++ b/Documents/api/index.md @@ -0,0 +1,31 @@ +<!-- +SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> +SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1 +--> + +# Tala website API + +Reference documentation of the website's ASP.NET Core API, generated by DocFX from the XML documentation comments +of `backend/Progmasoft.Tala.Website.Api`. The reference includes internal and private members: it documents the code +for the people who maintain it. + +Build and check it from the repository root: + +```sh +dotnet tool restore +dotnet docfx Documents/api/docfx.json --warningsAsErrors +dotnet run Documents/api/CheckDocumentation.cs -- Documents/api +``` + +## What the gate requires + +Three things fail the check, locally and in CI: + +- **A broken link or an unresolved reference.** DocFX runs with every warning treated as an error. +- **An undocumented public declaration or a wrong `cref`.** The API project compiles its XML documentation, and the + build treats the compiler's documentation warnings as errors. +- **Any undocumented item of the reference.** `CheckDocumentation.cs` reads the metadata DocFX generated and fails + when a namespace, type or member has no summary, internal and private members included. + +A namespace has no declaration that could hold a comment. Its description is a file in `namespaces/`, named after +the namespace. diff --git a/Documents/api/namespaces/Progmasoft.Tala.Website.Api.md b/Documents/api/namespaces/Progmasoft.Tala.Website.Api.md new file mode 100644 index 0000000..de90b81 --- /dev/null +++ b/Documents/api/namespaces/Progmasoft.Tala.Website.Api.md @@ -0,0 +1,10 @@ +--- +uid: Progmasoft.Tala.Website.Api +summary: *content +--- + +<!-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> --> +<!-- SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1 --> + +The API of the language website: the site's own public state at `/api/v1/site` and a liveness probe at `/health`. +It has no account, package, publication or registry endpoint. diff --git a/Documents/api/packages.lock.json b/Documents/api/packages.lock.json new file mode 100644 index 0000000..0f57a57 --- /dev/null +++ b/Documents/api/packages.lock.json @@ -0,0 +1,35 @@ +{ + "version": 1, + "dependencies": { + "net10.0": { + "Microsoft.DotNet.ILCompiler": { + "type": "Direct", + "requested": "[10.0.10, )", + "resolved": "10.0.10", + "contentHash": "tnG8ntt/Bk6odvHREnGLMo3PEiihy5iSlIFVp0JbIo00GKtNRt2k73eKZbPqR5yaJNIa3z8R86YLwbxfqpb17g==" + }, + "Microsoft.NET.ILLink.Tasks": { + "type": "Direct", + "requested": "[10.0.10, )", + "resolved": "10.0.10", + "contentHash": "f5VCIE7AJpd5YvzNTeMGVzQIgyE9tX+AreTYwQF+REbu+DZo/2Ae+jNSwhPEYrVz6RRkd7y8ubXjk6Nn6Ka+Cg==" + } + }, + "net10.0/win-x64": { + "Microsoft.DotNet.ILCompiler": { + "type": "Direct", + "requested": "[10.0.10, )", + "resolved": "10.0.10", + "contentHash": "tnG8ntt/Bk6odvHREnGLMo3PEiihy5iSlIFVp0JbIo00GKtNRt2k73eKZbPqR5yaJNIa3z8R86YLwbxfqpb17g==", + "dependencies": { + "runtime.win-x64.Microsoft.DotNet.ILCompiler": "10.0.10" + } + }, + "runtime.win-x64.Microsoft.DotNet.ILCompiler": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "e5P00ObvdmAl49KkvGvqDLE5+EXu/IdXCDsLghgKFXKrNybyxMmH12HuGrsO7LTIGjXI58tyN3Hwxwr/MR0d8A==" + } + } + } +}
\ No newline at end of file diff --git a/Documents/api/toc.yml b/Documents/api/toc.yml new file mode 100644 index 0000000..cf49dc1 --- /dev/null +++ b/Documents/api/toc.yml @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> +# SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1 + +- name: Overview + href: index.md +- name: Reference + href: reference/ @@ -77,7 +77,8 @@ dotnet test --project backend/Progmasoft.Tala.Website.Api.Tests/Progmasoft.Tala. dotnet publish backend/Progmasoft.Tala.Website.Api/Progmasoft.Tala.Website.Api.csproj --configuration Release --output publish ``` -The build treats warnings as errors and requires a documentation comment on every public declaration. The server unit +The build treats warnings as errors and requires a documentation comment on every public declaration; DocFX builds +the API reference from those comments and fails on any undocumented item (see `Documents/api/`). The server unit in `ops/systemd/tala-site-api.service` runs the published DLL from `/srv/xsharp/website/current/site-api/` on the shared ASP.NET Core 10 runtime and binds it only to loopback port 5086. The old .NET process on loopback port 5080 in `current/backend/` belongs to Progmaweb's ViGet status route; the language site does not proxy to it. Committing these @@ -5,6 +5,8 @@ SPDX-PackageDownloadLocation = "https://github.com/Progmasoft/website" [[annotations]] path = [ + ".config/dotnet-tools.json", + "Documents/api/docfx.json", "Progmasoft.Tala.Website.slnx", "global.json", "backend/Progmasoft.Tala.Website.Api/appsettings.json", |