diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 8a86cf29a..8793ee637 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -25,18 +25,30 @@ on: jobs: # ------------------------------------------------------------------ - # Job 0 — dotnet format gate. Verifies the tree matches the - # repository's .editorconfig with zero diff, so formatting drift + # Job 0 — dotnet format gate. Verifies the tree matches the default + # `dotnet format` ruleset with zero diff, so formatting drift # (indentation, brace placement, whitespace) is caught at PR time # instead of silently accumulating into the baseline every - # subsequent branch then has to inherit. Uses the default "warn" - # severity — the same severity `dotnet format` applies with no - # --severity flag — rather than --severity info: at info severity - # dotnet format additionally tries to auto-fix long-standing Roslyn - # analyzer diagnostics (CA1859, CA1861, CA1018, ...) across the - # repo, which is a materially different (and, on this SDK/analyzer - # combination, occasionally crash-prone) undertaking from verifying - # whitespace/style formatting and is out of scope for this gate. + # subsequent branch then has to inherit. The repo does not yet ship + # a root `.editorconfig`, so the enforced ruleset is whatever the + # SDK pinned by `actions/setup-dotnet` treats as its whitespace / + # C# formatting defaults; introducing a committed `.editorconfig` + # that freezes those rules against future SDK drift is tracked as a + # follow-up (see PR description). Uses the default "warn" severity + # — the same severity `dotnet format` applies with no --severity + # flag — rather than --severity info: at info severity dotnet format + # additionally tries to auto-fix long-standing Roslyn analyzer + # diagnostics (CA1859, CA1861, CA1018, ...) across the repo, which + # is a materially different (and, on this SDK/analyzer combination, + # occasionally crash-prone) undertaking from verifying whitespace / + # style formatting and is out of scope for this gate. `--verbosity + # diagnostic` is set on the verify step so the CI log names each + # offending file and rule when the gate fires — a contributor can + # then reproduce and fix locally without scrolling for the delta. + # A second verify step covers the two csprojs that ship with the + # `dotnet new` template but are not members of `MTConnect.NET.sln`; + # the template itself is a public-surface artifact for downstream + # consumers, so it belongs under the same gate as the main tree. # Runs on the same draft-skip gate as the other jobs and does not # depend on them, so a formatting-only fix gets fast feedback. # ------------------------------------------------------------------ @@ -58,8 +70,31 @@ jobs: - name: Restore solution run: dotnet restore MTConnect.NET.sln - - name: Verify format (dotnet format --verify-no-changes) - run: dotnet format MTConnect.NET.sln --verify-no-changes --no-restore + - name: Verify solution format (dotnet format --verify-no-changes) + run: dotnet format MTConnect.NET.sln --verify-no-changes --no-restore --verbosity diagnostic + + # `MTConnect.NET.sln` does not include the two csprojs that ship + # with the `dotnet new mtconnect.net-agent` template (the + # template descriptor + the generated project's csproj under + # `content/`). They are public-surface artifacts for downstream + # template consumers, so formatting drift there is exactly what + # the gate is meant to catch. Restore + verify them explicitly + # rather than adding them to the solution (which would compile + # them on every unrelated build and complicate the template + # packaging step). Each project gets its own restore + verify + # step so a failure names the specific template in the Actions + # step-summary panel rather than in the collapsed log. + - name: Restore template descriptor + run: dotnet restore templates/mtconnect.net-agent/MTConnect-NET-Agent-Template.csproj + + - name: Restore template content project + run: dotnet restore templates/mtconnect.net-agent/content/MTConnect.NET-Embedded-Agent/Agent.csproj + + - name: Verify template descriptor format + run: dotnet format templates/mtconnect.net-agent/MTConnect-NET-Agent-Template.csproj --verify-no-changes --no-restore --verbosity diagnostic + + - name: Verify template content project format + run: dotnet format templates/mtconnect.net-agent/content/MTConnect.NET-Embedded-Agent/Agent.csproj --verify-no-changes --no-restore --verbosity diagnostic # ------------------------------------------------------------------ # Job 1 — unsharded unit + integration sweep on both OS legs. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..fa269cf35 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,43 @@ +# Contributing to MTConnect.NET + +Thank you for taking the time to contribute — issues, discussions, and pull requests are all welcome. This file collects the mechanical checks a contributor is expected to run locally before pushing; the substantive discussion of what to work on lives on the [MTConnect.NET issue tracker](https://github.com/TrakHound/MTConnect.NET/issues) and on the [Documentation site](https://trakhound.github.io/MTConnect.NET/). + +## Running the formatting gate locally + +Every push to `master` and every non-draft pull request runs `dotnet format MTConnect.NET.sln --verify-no-changes` in CI (job `format`, defined in [`.github/workflows/dotnet.yml`](.github/workflows/dotnet.yml)). The gate rejects any whitespace / indentation / brace-placement drift, so it is worth reproducing the check before pushing: + +```bash +dotnet restore MTConnect.NET.sln +dotnet format MTConnect.NET.sln # autofix in place +dotnet format MTConnect.NET.sln --verify-no-changes --verbosity diagnostic # dry-run, same as CI +``` + +The two `dotnet new mtconnect.net-agent` template projects live outside the solution and CI verifies them in dedicated steps; run the same commands against them if you have touched files under `templates/`: + +```bash +dotnet restore templates/mtconnect.net-agent/MTConnect-NET-Agent-Template.csproj +dotnet restore templates/mtconnect.net-agent/content/MTConnect.NET-Embedded-Agent/Agent.csproj +dotnet format templates/mtconnect.net-agent/MTConnect-NET-Agent-Template.csproj --verify-no-changes --verbosity diagnostic +dotnet format templates/mtconnect.net-agent/content/MTConnect.NET-Embedded-Agent/Agent.csproj --verify-no-changes --verbosity diagnostic +``` + +See [`docs/testing/workflows.md`](docs/testing/workflows.md#job-0--format) for the complete gate description, the `--severity warn` rationale, and the tracked follow-ups (root `.editorconfig` + `global.json` SDK pin). + +## Running the test suite locally + +The default sweep runs unit and light-integration tests across the whole solution: + +```bash +./tools/test.sh # Linux / macOS / Git Bash +./tools/test.ps1 # PowerShell, all platforms +``` + +Add `--e2e` / `-E2E` (Docker required) to also run the `Category=E2E` and `Category=RequiresDocker` workflow fixtures. `./tools/test.sh --help` (or `./tools/test.ps1 -?`) prints the full flag listing. The CI matrix and per-category filter rationale are documented on [`docs/testing/workflows.md`](docs/testing/workflows.md). + +## Opening a pull request + +- Keep the PR body focused on the user-facing diff — what changed, why it changed, and how a reviewer can verify it. Internal process detail belongs in the commit trailer or in follow-up comments, not in the description. +- Every push re-runs the full CI matrix on non-draft PRs. Leave a PR in draft while it is still being iterated; flip it to ready when it is ready for review. +- If your change touches the `templates/` tree, please also run the template-project format commands above — the CI gate will catch drift, but reproducing locally saves a red-CI round-trip. + +Thanks again for your interest in improving MTConnect.NET. diff --git a/README.md b/README.md index 8bd2143a4..8236ba04b 100644 --- a/README.md +++ b/README.md @@ -246,6 +246,18 @@ This repo along with the libraries and applications are free to use and distribu Feel free to comment, or create pull-requests for anything that could be coded, formatted, or worded better. Attention to detail and continuous improvement are important in manufacturing so they should be just as important for manufacturing software. +### Contributing + +See [`CONTRIBUTING.md`](CONTRIBUTING.md) for the local-reproduction commands the CI gate expects (formatting + tests) and the pull-request expectations. In short, before pushing: + +```bash +dotnet restore MTConnect.NET.sln +dotnet format MTConnect.NET.sln --verify-no-changes --verbosity diagnostic # same as the CI `format` job +./tools/test.sh # or ./tools/test.ps1 +``` + +The full CI gate description, the `--severity warn` rationale, and the tracked follow-ups (root `.editorconfig` + `global.json` SDK pin) are documented on [`docs/testing/workflows.md`](docs/testing/workflows.md#job-0--format). + Thanks for your interest in using these libraries and applications and feel free to contribute or give feedback. \- Patrick diff --git a/build/MTConnect.NET-DocsGen/RouteInventory.cs b/build/MTConnect.NET-DocsGen/RouteInventory.cs index 1cfcf2aca..c4e1fb5a6 100644 --- a/build/MTConnect.NET-DocsGen/RouteInventory.cs +++ b/build/MTConnect.NET-DocsGen/RouteInventory.cs @@ -218,65 +218,88 @@ private static (string? method, string? handler) SniffLambdaGate(ParenthesizedLa // imperatively inside OnRequestReceived, so the surface is encoded // structurally here to give the reference page meaningful columns; // the *summary text* still flows from /// on the handler class. + // + // Each handler's parameter list is a separate `private static readonly` + // field rather than an inline nested initializer inside the + // dictionary literal. `dotnet format` cannot indent an inline + // `new EndpointParam[] { ... }` nested inside a dictionary + // collection-initializer coherently — it deepens the outer keys + // but leaves the inner block at the old depth, producing a visible + // misalignment cascade. Hoisting the arrays flattens both layers to + // the same indent depth and keeps the formatter idempotent. + private static readonly EndpointParam[] ProbeParams = + { + new("deviceType", "Query", "string", null, "Optional device-type filter."), + new("version", "Query", "string", null, "Target MTConnect Standard version of the response document."), + new("documentFormat", "Query", "string", "xml", "Response document format (xml | json | json-cppagent)."), + new("validationLevel", "Query", "int", null, "0=ignore, 1=warning, 2=remove, 3=strict."), + new("indentOutput", "Query", "bool", null, "Pretty-print the response document."), + new("outputComments", "Query", "bool", null, "Emit comments / annotations in the response document."), + }; + + private static readonly EndpointParam[] CurrentParams = + { + new("path", "Query", "string", null, "XPath that filters the data items included in the response."), + new("at", "Query", "ulong", null, "Sequence number anchoring the snapshot."), + new("interval", "Query", "int", null, "Streaming interval in milliseconds; switches to multipart streaming when > 0."), + new("heartbeat", "Query", "int", "10000", "Heartbeat interval for the streaming response."), + new("deviceType", "Query", "string", null, "Optional device-type filter."), + new("version", "Query", "string", null, "Target MTConnect Standard version of the response document."), + new("documentFormat", "Query", "string", "xml", "Response document format."), + new("indentOutput", "Query", "bool", null, "Pretty-print the response document."), + new("outputComments", "Query", "bool", null, "Emit comments / annotations in the response document."), + }; + + private static readonly EndpointParam[] SampleParams = + { + new("path", "Query", "string", null, "XPath that filters the data items included in the response."), + new("from", "Query", "ulong", null, "Sequence number lower bound."), + new("to", "Query", "ulong", null, "Sequence number upper bound."), + new("count", "Query", "int", "100", "Maximum number of observations."), + new("interval", "Query", "int", null, "Streaming interval in milliseconds; switches to multipart streaming when > 0."), + new("heartbeat", "Query", "int", "10000", "Heartbeat interval for the streaming response."), + new("deviceType", "Query", "string", null, "Optional device-type filter."), + new("version", "Query", "string", null, "Target MTConnect Standard version of the response document."), + new("documentFormat", "Query", "string", "xml", "Response document format."), + new("indentOutput", "Query", "bool", null, "Pretty-print the response document."), + new("outputComments", "Query", "bool", null, "Emit comments / annotations in the response document."), + }; + + private static readonly EndpointParam[] AssetsParams = + { + new("type", "Query", "string", null, "Asset type filter (e.g. CuttingTool)."), + new("removed", "Query", "bool", null, "Include removed assets when true."), + new("count", "Query", "int", null, "Maximum number of assets."), + new("documentFormat", "Query", "string", "xml", "Response document format."), + new("indentOutput", "Query", "bool", null, "Pretty-print the response document."), + }; + + private static readonly EndpointParam[] AssetParams = + { + new("assetId", "Route", "string", null, "Asset identifier captured from the trailing path segment."), + new("documentFormat", "Query", "string", "xml", "Response document format."), + }; + + private static readonly EndpointParam[] PutParams = + { + new("(form / query)", "Body", "Dictionary", null, "DataItemId=Value entries to enqueue as observations."), + }; + + private static readonly EndpointParam[] PostParams = + { + new("(body)", "Body", "string", null, "Asset document payload."), + }; + private static readonly IReadOnlyDictionary> CeenHandlerParameters = new Dictionary> { - ["MTConnectProbeResponseHandler"] = new EndpointParam[] - { - new("deviceType", "Query", "string", null, "Optional device-type filter."), - new("version", "Query", "string", null, "Target MTConnect Standard version of the response document."), - new("documentFormat", "Query", "string", "xml", "Response document format (xml | json | json-cppagent)."), - new("validationLevel", "Query", "int", null, "0=ignore, 1=warning, 2=remove, 3=strict."), - new("indentOutput", "Query", "bool", null, "Pretty-print the response document."), - new("outputComments", "Query", "bool", null, "Emit comments / annotations in the response document."), - }, - ["MTConnectCurrentResponseHandler"] = new EndpointParam[] - { - new("path", "Query", "string", null, "XPath that filters the data items included in the response."), - new("at", "Query", "ulong", null, "Sequence number anchoring the snapshot."), - new("interval", "Query", "int", null, "Streaming interval in milliseconds; switches to multipart streaming when > 0."), - new("heartbeat", "Query", "int", "10000", "Heartbeat interval for the streaming response."), - new("deviceType", "Query", "string", null, "Optional device-type filter."), - new("version", "Query", "string", null, "Target MTConnect Standard version of the response document."), - new("documentFormat", "Query", "string", "xml", "Response document format."), - new("indentOutput", "Query", "bool", null, "Pretty-print the response document."), - new("outputComments", "Query", "bool", null, "Emit comments / annotations in the response document."), - }, - ["MTConnectSampleResponseHandler"] = new EndpointParam[] - { - new("path", "Query", "string", null, "XPath that filters the data items included in the response."), - new("from", "Query", "ulong", null, "Sequence number lower bound."), - new("to", "Query", "ulong", null, "Sequence number upper bound."), - new("count", "Query", "int", "100", "Maximum number of observations."), - new("interval", "Query", "int", null, "Streaming interval in milliseconds; switches to multipart streaming when > 0."), - new("heartbeat", "Query", "int", "10000", "Heartbeat interval for the streaming response."), - new("deviceType", "Query", "string", null, "Optional device-type filter."), - new("version", "Query", "string", null, "Target MTConnect Standard version of the response document."), - new("documentFormat", "Query", "string", "xml", "Response document format."), - new("indentOutput", "Query", "bool", null, "Pretty-print the response document."), - new("outputComments", "Query", "bool", null, "Emit comments / annotations in the response document."), - }, - ["MTConnectAssetsResponseHandler"] = new EndpointParam[] - { - new("type", "Query", "string", null, "Asset type filter (e.g. CuttingTool)."), - new("removed", "Query", "bool", null, "Include removed assets when true."), - new("count", "Query", "int", null, "Maximum number of assets."), - new("documentFormat", "Query", "string", "xml", "Response document format."), - new("indentOutput", "Query", "bool", null, "Pretty-print the response document."), - }, - ["MTConnectAssetResponseHandler"] = new EndpointParam[] - { - new("assetId", "Route", "string", null, "Asset identifier captured from the trailing path segment."), - new("documentFormat", "Query", "string", "xml", "Response document format."), - }, - ["MTConnectPutResponseHandler"] = new EndpointParam[] - { - new("(form / query)", "Body", "Dictionary", null, "DataItemId=Value entries to enqueue as observations."), - }, - ["MTConnectPostResponseHandler"] = new EndpointParam[] - { - new("(body)", "Body", "string", null, "Asset document payload."), - }, + ["MTConnectProbeResponseHandler"] = ProbeParams, + ["MTConnectCurrentResponseHandler"] = CurrentParams, + ["MTConnectSampleResponseHandler"] = SampleParams, + ["MTConnectAssetsResponseHandler"] = AssetsParams, + ["MTConnectAssetResponseHandler"] = AssetParams, + ["MTConnectPutResponseHandler"] = PutParams, + ["MTConnectPostResponseHandler"] = PostParams, }; // Cache of parsed /// summary text per handler-class file path, so diff --git a/docs/testing/workflows.md b/docs/testing/workflows.md index 0e2f9f988..4f3c8079c 100644 --- a/docs/testing/workflows.md +++ b/docs/testing/workflows.md @@ -49,6 +49,54 @@ non-draft PR against `master` (drafts skip; flipping ready fires the run on `ready_for_review`). **Matrix:** `ubuntu-latest` × `windows-latest`, .NET SDK `8.0.x` + `9.0.x`. +The `format` gate is single-legged (`ubuntu-latest`) because +`dotnet format`'s output is deterministic across OSes — running it +on both would only pay the wall-clock twice for the same result. + +### Job 0 — `format` + +Verifies the whole tree is byte-identical to `dotnet format`'s +whitespace / C# formatting output, so indentation, brace placement, +and trailing-whitespace drift cannot silently reaccumulate into the +baseline. Runs on `ubuntu-latest`, installs both .NET 8.0.x and +9.0.x (the same pair the build / test / docs / e2e jobs use), and +does not depend on any other job — a formatting-only fix therefore +gets fast feedback without waiting on the unit + integration matrix. + +1. Checkout (`actions/checkout`). +2. Setup .NET (`actions/setup-dotnet`) — installs both 8.0.x and 9.0.x. +3. `dotnet restore MTConnect.NET.sln`. +4. `dotnet format MTConnect.NET.sln --verify-no-changes --no-restore --verbosity diagnostic` + — exits non-zero on any diff; `--verbosity diagnostic` names each + offending file and rule directly in the CI log so a contributor + can reproduce and fix locally without further tooling. +5. `dotnet restore` + `dotnet format --verify-no-changes` for the two + template csprojs that are not solution members + (`templates/mtconnect.net-agent/MTConnect-NET-Agent-Template.csproj` + and `.../content/MTConnect.NET-Embedded-Agent/Agent.csproj`). Both + ship to downstream consumers via `dotnet new`, so their formatting + is user-facing and belongs under the same gate. + +The gate enforces `dotnet format`'s default `warn` severity, not +`--severity info`: at `info`, `dotnet format` additionally auto-fixes +long-standing Roslyn analyzer diagnostics (`CA1859`, `CA1861`, +`CA1018`, ...) across the whole repo, which is a materially larger +undertaking than whitespace verification (and, on the SDK / analyzer +combination this repo targets, occasionally aborts mid-run with +`NotSupportedException`). Analyzer-diagnostic cleanup is tracked as +its own follow-up rather than folded into this gate. + +**Local reproduction:** `dotnet format MTConnect.NET.sln` (autofix) +or `dotnet format MTConnect.NET.sln --verify-no-changes --verbosity diagnostic` +(dry-run, exits non-zero on any diff — the same invocation CI uses). +The repo does not yet ship a root `.editorconfig`; the gate therefore +relies on the SDK's built-in formatting defaults, and a future SDK +release that shifts those defaults will re-fire the gate on unchanged +code. Freezing the ruleset with a committed `.editorconfig` + +`global.json` SDK pin is a tracked follow-up (see the format-baseline +PR description). + +### Jobs 1–4 — build, test, docs, route-check **Steps:**