diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 274dcb87..05a4a576 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,6 +2,20 @@ Thank you for your interest in improving this library! +## Developer Documentation + +In-depth developer guides are in the [`/docs`](./docs/README.md) folder: + +- [Local Development](./docs/local-development.md) — build, test, and format commands +- [Testing](./docs/testing.md) — how to write unit tests +- [Benchmarks](./docs/benchmarks.md) — how to write and run benchmarks +- [Composite Actions](./docs/composite-actions.md) — reusable CI actions catalogue +- [Workflows](./docs/workflows.md) — CI/CD pipeline overview +- [Branch Strategy](./docs/branch-strategy.md) — branch lifecycle and environments +- [Versioning](./docs/versioning.md) — branch naming and the version pipeline +- [API Documentation](./docs/api-documentation.md) — DocFX and the API reference site +- [Extensibility](./docs/extensibility.md) — how to add new encoding algorithms + ## Guidelines - **Follow code style:** Use `.editorconfig` and run `dotnet format`. diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..fadcbe7d --- /dev/null +++ b/docs/README.md @@ -0,0 +1,23 @@ +# Developer Documentation + +Welcome to the developer documentation for **PolylineAlgorithm**. This section covers everything you need to contribute code, tests, benchmarks, and automation to the project. + +## Contents + +| Document | Description | +|---|---| +| [Local Development](./local-development.md) | Build, test, and format the codebase locally | +| [Testing](./testing.md) | How to write and run unit tests | +| [Benchmarks](./benchmarks.md) | How to write and run performance benchmarks | +| [Composite Actions](./composite-actions.md) | Reusable GitHub Actions used across workflows | +| [Workflows](./workflows.md) | CI/CD pipelines and how they connect | +| [Branch Strategy](./branch-strategy.md) | Branch lifecycle and environment mapping | +| [Versioning](./versioning.md) | Branch naming convention and the version pipeline | +| [API Documentation](./api-documentation.md) | How DocFX generates and publishes the API reference site | +| [Extensibility](./extensibility.md) | How to add new encoding algorithms | + +## Quick Links + +- [Contributing Guidelines](../CONTRIBUTING.md) +- [API Reference Site](https://petesramek.github.io/polyline-algorithm-csharp/) +- [GitHub Issues](https://github.com/petesramek/polyline-algorithm-csharp/issues) diff --git a/docs/api-documentation.md b/docs/api-documentation.md new file mode 100644 index 00000000..d709fe74 --- /dev/null +++ b/docs/api-documentation.md @@ -0,0 +1,136 @@ +# API Documentation + +This document explains how API documentation is generated and published for PolylineAlgorithm. + +## Toolchain + +Documentation is built with [DocFX](https://dotnet.github.io/docfx/), a static site generator for .NET API references and Markdown guides. + +## Repository Layout + +``` +api-reference/ +├── api-reference.json # DocFX build manifest (site generation) +├── assembly-metadata.json # DocFX metadata manifest (API extraction only) +├── toc.yml # Top-level table of contents +├── index.md # Landing page +├── favicon.ico +├── media/ # Images and other static assets +├── guide/ # Markdown guide articles +│ ├── toc.yml +│ ├── introduction.md +│ ├── getting-started.md +│ ├── configuration.md +│ ├── advanced-scenarios.md +│ ├── sample.md +│ └── faq.md +├── 0.0/ # Auto-generated API metadata for version 0.0 +├── 1.0/ # Auto-generated API metadata for version 1.0 +│ └── *.yml # DocFX apiPage YAML files (one per type) +└── _docs/ # Build output (excluded from DocFX content, gitignored) +``` + +## Two DocFX Manifests + +### `assembly-metadata.json` — API Extraction + +Used by the `documentation/docfx-metadata` composite action during CI builds to extract XML documentation comments from source code and produce DocFX-compatible YAML files. + +```json +{ + "metadata": [{ + "src": [{ "src": "../src", "files": ["**/*.csproj"] }], + "dest": "temp", + "outputFormat": "apiPage" + }] +} +``` + +- **Input:** All `.csproj` files under `src/` +- **Output:** YAML files in `api-reference/temp/`, then copied to `api-reference//` +- **When it runs:** Automatically after every successful `build` workflow run. The resulting YAML files are committed to the repository under `api-reference//` (e.g., `api-reference/1.2/`). + +### `api-reference.json` — Site Build + +Used by the `documentation/docfx-build` composite action to build the full documentation site. + +```json +{ + "build": { + "content": [ + { "files": ["index.md", "toc.yml", "guide/*.{md,yml}"], "exclude": ["_docs/**"] }, + { "dest": "", "files": ["*.yml"], "group": "v1.0", "src": "1.0" }, + { "dest": "", "files": ["*.yml"], "group": "v1.1", "src": "1.1" } + ], + "output": "_docs", + "template": ["default", "modern"] + } +} +``` + +- **Input:** Markdown guide articles + versioned API YAML files +- **Output:** Static HTML site in `api-reference/_docs/` +- **When it runs:** During `release.yml` (automatic on every push to `preview/**` or `release/**`) and `publish-documentation.yml` (manual trigger). + +## Publishing Flow + +``` +src/ changed + │ + ▼ +[build.yml] → docfx metadata → commit YAML to api-reference// + │ + ▼ + [release.yml] or [publish-documentation.yml] + │ + ▼ + docfx build → api-reference/_docs/ + │ + ▼ + GitHub Pages → petesramek.github.io/polyline-algorithm-csharp +``` + +## Adding a New Version to the Site + +When bumping the version to `X.Y`: + +1. The `build` workflow automatically generates metadata YAML files into `api-reference/X.Y/` after the first build on the new branch. +2. Add a new entry to `api-reference.json` under `build.content`: + ```json + { "dest": "", "files": ["*.yml"], "group": "vX.Y", "src": "X.Y", "rootTocPath": "~/toc.html" } + ``` +3. Add a matching group definition: + ```json + "vX.Y": { "dest": "X.Y" } + ``` +4. Add the new version to `api-reference/toc.yml` so it appears in the navigation dropdown: + ```yaml + - name: vX.Y + href: X.Y/PolylineAlgorithm.html + ``` + +## Writing API Documentation + +All public types, interfaces, and members must have XML doc comments. DocFX picks these up automatically: + +```csharp +/// +/// Encodes a sequence of coordinates into a polyline string. +/// +/// The coordinates to encode. +/// An encoded polyline string. +public string Encode(IEnumerable<(double Latitude, double Longitude)> coordinates) { ... } +``` + +After merging a change, verify the rendered documentation at the [API Reference Site](https://petesramek.github.io/polyline-algorithm-csharp/). + +## Local Preview + +To preview the documentation locally: + +```bash +dotnet tool update -g docfx +docfx build ./api-reference/api-reference.json --serve +``` + +Then open `http://localhost:8080` in your browser. diff --git a/docs/benchmarks.md b/docs/benchmarks.md new file mode 100644 index 00000000..0f15e44d --- /dev/null +++ b/docs/benchmarks.md @@ -0,0 +1,133 @@ +# Benchmarks + +This guide explains the benchmark project structure and how to write and run performance benchmarks. + +## Project Structure + +All benchmarks live in the `benchmarks/` directory: + +``` +benchmarks/ +└── PolylineAlgorithm.Benchmarks/ + ├── PolylineEncoderBenchmark.cs # Benchmarks for AbstractPolylineEncoder + ├── PolylineDecoderBenchmark.cs # Benchmarks for PolylineDecoder + ├── PolylineEncodingBenchmark.cs # Benchmarks for PolylineEncoding helpers + ├── Program.cs # BenchmarkSwitcher entry point + └── PolylineAlgorithm.Benchmarks.csproj +``` + +The project targets `net8.0`, `net9.0`, and `net10.0` and references the main `PolylineAlgorithm` library along with the `PolylineAlgorithm.Utility` helper project. + +## Framework + +Benchmarks use [BenchmarkDotNet](https://benchmarkdotnet.org/). Key packages: + +| Package | Purpose | +|---|---| +| `BenchmarkDotNet` | Core benchmarking framework | +| `BenchmarkDotNet.Diagnostics.Windows` | Windows-specific diagnostics (ETW) | + +## Writing a New Benchmark + +1. Create a new `.cs` file in `benchmarks/PolylineAlgorithm.Benchmarks/`. +2. Add the standard copyright header. +3. Annotate the class with `[MemoryDiagnoser]` if you want allocation tracking. +4. Use `[Params]` to parameterize input sizes. +5. Mark benchmark methods with `[Benchmark]`. Mark one with `[Benchmark(Baseline = true)]` when comparing variants. +6. Use `[GlobalSetup]` to prepare shared data once per parameter combination. + +Example: + +```csharp +// +// Copyright © Pete Sramek. All rights reserved. +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace PolylineAlgorithm.Benchmarks; + +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Engines; + +/// +/// Benchmarks for . +/// +[MemoryDiagnoser] +public class MyEncoderBenchmark { + private readonly Consumer _consumer = new(); + + [Params(1, 100, 1_000)] + public int CoordinatesCount { get; set; } + + private (double Latitude, double Longitude)[] _data = []; + + [GlobalSetup] + public void Setup() { + _data = [.. RandomValueProvider.GetCoordinates(CoordinatesCount)]; + } + + [Benchmark(Baseline = true)] + public void EncodeArray() => new MyEncoder().Encode(_data).Consume(_consumer); +} +``` + +## Running Benchmarks Locally + +Benchmarks **must** run in Release configuration to produce meaningful results: + +```bash +dotnet run \ + --project ./benchmarks/PolylineAlgorithm.Benchmarks/PolylineAlgorithm.Benchmarks.csproj \ + --configuration Release \ + --framework net10.0 \ + -- --filter '*' +``` + +### Useful CLI flags + +| Flag | Description | +|---|---| +| `--filter '*'` | Run all benchmarks | +| `--filter '*Encoder*'` | Run benchmarks whose name contains `Encoder` | +| `--runtimes net8.0 net9.0 net10.0` | Run on multiple runtimes | +| `--exporters GitHub` | Export results as GitHub Flavored Markdown | +| `--memory` | Enable memory diagnoser output | +| `--iterationTime 100` | Iteration time in milliseconds | +| `--join` | Merge results from multiple runs | +| `--artifacts ` | Output directory for results | + +### Example: multi-runtime run with GitHub export + +```bash +dotnet run \ + --project ./benchmarks/PolylineAlgorithm.Benchmarks/PolylineAlgorithm.Benchmarks.csproj \ + --configuration Release \ + --framework net10.0 \ + -- --runtimes net8.0 net9.0 net10.0 \ + --filter '*' \ + --exporters GitHub \ + --memory \ + --iterationTime 100 \ + --join \ + --artifacts /tmp/benchmarks +``` + +## Benchmarks in CI + +The `pull-request` workflow runs benchmarks on Ubuntu, Windows, and macOS when `vars.BENCHMARKDOTNET_RUN_OVERRIDE == 'true'` or when building a release branch. Results are uploaded as artifacts (`benchmark-`) and written to the workflow step summary as a Markdown table. + +Relevant workflow variables: + +| Variable | Description | +|---|---| +| `BENCHMARKDOTNET_WORKING_DIRECTORY` | Working directory for `dotnet run` | +| `BENCHMARKDOTNET_RUNTIMES` | Space-separated runtimes to bench | +| `BENCHMARKDOTNET_FILTER` | Filter expression passed to `--filter` | +| `DEFAULT_BUILD_FRAMEWORK` | Framework used with `--framework` | +| `BENCHMARKDOTNET_RUN_OVERRIDE` | Set to `true` to force benchmark run on PRs | + +## When to Add or Update Benchmarks + +- Add a new benchmark file when introducing a new encoding/decoding code path. +- Update an existing benchmark when changing the algorithmic implementation of an existing path. +- Attach benchmark results to pull requests that affect performance-sensitive code (see [CONTRIBUTING.md](../CONTRIBUTING.md)). diff --git a/docs/branch-strategy.md b/docs/branch-strategy.md new file mode 100644 index 00000000..aa640ee7 --- /dev/null +++ b/docs/branch-strategy.md @@ -0,0 +1,87 @@ +# Branch Strategy + +This document describes the branch model, the purpose of each branch type, and how a change moves from a feature branch all the way to a stable release. + +## Branch Types + +| Pattern | Purpose | Protected | +|---|---|---| +| `main` | Latest stable source of truth | ✅ Yes | +| `develop/**` | Active feature development | ❌ No | +| `support/**` | Maintenance / backport development | ❌ No | +| `preview/X.Y` | Pre-release stabilization | ✅ Yes (1 approval required) | +| `release/X.Y` | Release stabilization | ✅ Yes (1 approval required) | + +## Change Lifecycle + +``` +1. Feature work + └─ develop/my-feature (or support/my-fix for backports) + │ + │ push to src/ → [build.yml] runs: format, compile, test, pack, publish-dev + │ +2. Promote to preview + └─ promote-branch.yml (manual) → creates preview/X.Y + PR: develop → preview/X.Y + │ + │ PR open → [pull-request.yml]: compile, test, pack, benchmark (optional) + │ PR merged → [release.yml]: compile, test, pack, publish-NuGet (pre-release), GitHub release, docs + │ +3. Promote to release + └─ promote-branch.yml (manual) → creates release/X.Y + PR: preview/X.Y → release/X.Y + │ + │ PR open → [pull-request.yml] + │ PR merged → [release.yml]: publish-NuGet (stable), GitHub release, docs + │ +4. Back-merge (optional) + └─ Manual PR: release/X.Y → main +``` + +## Rules Per Branch Type + +### `main` + +- Represents the current stable release. +- Direct pushes are not allowed (protected). +- Updated by merging from `release/X.Y` after a stable release. +- The `build.yml` workflow does **not** trigger on `main` pushes (branch-ignore pattern excludes `preview/**` and `release/**`, and `main` does not match `src/**` changes by default in the context of the ignore rules — check the workflow for current specifics). + +### `develop/**` + +- Naming convention: `develop/` (e.g. `develop/async-decoder`, `develop/1.2`). +- The `build.yml` CI pipeline runs on every push to `src/`. +- When ready for stabilization, use `promote-branch.yml` to create a `preview/X.Y` branch and open a PR. + +### `support/**` + +- Used for backport and maintenance work against older versions. +- Same CI behavior as `develop/**`. +- Can be promoted to `preview/X.Y` for a patch release. + +### `preview/X.Y` + +- Created automatically by `promote-branch.yml`. +- Locked immediately: requires at least one PR approval before any merge. +- The `pull-request.yml` workflow runs on every PR targeting this branch. +- On merge, `release.yml` publishes a **pre-release** NuGet package. +- When all pre-release validation is done, promote to `release/X.Y`. + +### `release/X.Y` + +- Created automatically by `promote-branch.yml` from `preview/X.Y`. +- Locked immediately: requires at least one PR approval. +- On merge, `release.yml` publishes a **stable** NuGet package and a GitHub release. + +## Version in Branch Names + +The `X.Y` in `preview/X.Y` and `release/X.Y` drives the version pipeline. See [Versioning](./versioning.md) for details. + +## Environments + +| GitHub Environment | Used by | NuGet feed | +|---|---|---| +| `Development` | `build.yml`, `pull-request.yml` | Azure Artifacts | +| `Production` | `release.yml` | NuGet.org | + +## Locking and Unlocking Branches + +`preview/**` and `release/**` branches are locked via the [`github/branch-protection/lock`](./composite-actions.md#githubbranch-protectionlock) composite action when created. The [`github/branch-protection/unlock`](./composite-actions.md#githubbranch-protectionunlock) action temporarily removes protection when a workflow needs to push directly (e.g., `bump-version.yml`). Branches are always re-locked immediately after. diff --git a/docs/composite-actions.md b/docs/composite-actions.md new file mode 100644 index 00000000..66243f45 --- /dev/null +++ b/docs/composite-actions.md @@ -0,0 +1,307 @@ +# Composite Actions + +All reusable GitHub Actions live under `.github/actions/`. They are referenced with `uses: './.github/actions/'` inside workflows. This document catalogs each action, its inputs, outputs, and typical use. + +## documentation/docfx-build + +**Path:** `.github/actions/documentation/docfx-build` +**Description:** Installs the `docfx` global tool, builds a DocFX site from a JSON manifest, and uploads the generated output as a workflow artifact. + +| Input | Required | Default | Description | +|---|---|---|---| +| `artifact-name` | ✅ | — | Name of the uploaded artifact | +| `docfx-json-manifest` | ✅ | — | Path to the `docfx.json` build manifest | +| `output-directory` | ✅ | — | Target directory where DocFX writes output | +| `dotnet_sdk_version` | ❌ | `10.x` | .NET SDK version to use | + +**Used by:** `publish-documentation` workflow. + +--- + +## documentation/docfx-metadata + +**Path:** `.github/actions/documentation/docfx-metadata` +**Description:** Runs `docfx metadata` to extract API metadata from source (`.yml` files), copies the result to an output directory, and uploads it as a workflow artifact. + +| Input | Required | Default | Description | +|---|---|---|---| +| `artifact-name` | ✅ | — | Name of the uploaded artifact | +| `docfx-json-manifest` | ✅ | — | Path to the metadata-only `docfx.json` manifest | +| `temporary-directory` | ✅ | — | Temp folder DocFX writes raw metadata into | +| `output-directory` | ✅ | — | Final output directory for the metadata | +| `dotnet_sdk_version` | ❌ | `10.x` | .NET SDK version to use | + +**Used by:** `build` workflow (to regenerate versioned assembly metadata). + +--- + +## git/push-changes + +**Path:** `.github/actions/git/push-changes` +**Description:** Optionally downloads an artifact, stages all changes in a working directory, and pushes them to the current (or a specified target) branch. Skips the commit if there are no staged changes. + +| Input | Required | Default | Description | +|---|---|---|---| +| `commit-message` | ✅ | — | Commit message | +| `artifact-name` | ❌ | `''` | Artifact to download before staging | +| `working-directory` | ❌ | `.` | Directory to stage and commit from | +| `target-branch` | ❌ | `''` | Branch to push to (creates it if absent) | +| `dotnet_sdk_version` | ❌ | `10.x` | .NET SDK version | + +**Used by:** `source/format`, `build` (assembly metadata), and other workflows that commit generated artefacts. + +--- + +## github/branch-protection/lock + +**Path:** `.github/actions/github/branch-protection/lock` +**Description:** Applies branch protection rules to a branch via the GitHub API: requires at least one PR approval, disables force pushes and deletions. + +| Input | Required | Description | +|---|---|---| +| `branch` | ✅ | Branch name to protect | + +**Requires:** `administration: write` permission on the workflow token. +**Used by:** `promote-branch` workflow (after creating a new `preview/**` or `release/**` branch). + +--- + +## github/branch-protection/unlock + +**Path:** `.github/actions/github/branch-protection/unlock` +**Description:** Removes all branch protection rules from a branch so a workflow can push directly to it. Always re-lock immediately after. + +| Input | Required | Description | +|---|---|---| +| `branch` | ✅ | Branch name to unprotect | + +**Requires:** `administration: write` permission on the workflow token. +**Used by:** Workflows that need to push commits directly to protected branches (e.g., `bump-version`). + +--- + +## github/create-release + +**Path:** `.github/actions/github/create-release` +**Description:** Creates a git tag and a GitHub release with auto-generated release notes. Supports pre-release flag and a notes-start-tag for scoping the changelog. + +| Input | Required | Default | Description | +|---|---|---|---| +| `release-version` | ✅ | — | SemVer string used for both the tag and the release name | +| `is-preview` | ✅ | — | `'true'` marks the release as pre-release | +| `notes-start-tag` | ❌ | `''` | Git tag from which to start auto-generated notes | + +**Used by:** `release` workflow. + +--- + +## github/write-file-to-summary + +**Path:** `.github/actions/github/write-file-to-summary` +**Description:** Appends the contents of a file (matched by glob) to the GitHub step summary (`GITHUB_STEP_SUMMARY`). + +| Input | Required | Default | Description | +|---|---|---|---| +| `file-glob-pattern` | ✅ | — | Glob pattern for the file(s) to append | +| `working-directory` | ❌ | `${{ github.workspace }}` | Directory to resolve the glob against | + +--- + +## nuget/publish-package + +**Path:** `.github/actions/nuget/publish-package` +**Description:** Downloads a NuGet package artifact and pushes it to either a public NuGet feed or an Azure Artifacts feed. Validates the `nuget-feed-server` value before proceeding. + +| Input | Required | Default | Description | +|---|---|---|---| +| `package-artifact-name` | ✅ | — | Name of the artifact containing `.nupkg` files | +| `nuget-feed-url` | ✅ | — | Feed endpoint URL | +| `nuget-feed-api-key` | ✅ | — | API key / PAT for the feed | +| `nuget-feed-server` | ✅ | — | `'NuGet'` or `'AzureArtifacts'` | +| `dotnet-sdk-version` | ❌ | `10.x` | .NET SDK version | +| `working-directory` | ❌ | `${{ github.workspace }}` | Directory containing `.nupkg` files | + +**Used by:** `build` (Development environment) and `release` (NuGet.org) workflows. + +--- + +## source/compile + +**Path:** `.github/actions/source/compile` +**Description:** Builds a project in Release configuration, injecting version properties, and uploads the binary output as a workflow artifact. + +| Input | Required | Default | Description | +|---|---|---|---| +| `assembly-version` | ✅ | — | `AssemblyVersion` MSBuild property | +| `assembly-informational-version` | ✅ | — | `AssemblyInformationalVersion` MSBuild property | +| `file-version` | ✅ | — | `FileVersion` MSBuild property | +| `treat-warnins-as-error` | ✅ | — | When `'true'`, runs `dotnet format analyzers --verify-no-changes` | +| `project-path` | ✅ | — | Glob pattern for project files | +| `dotnet_sdk_version` | ❌ | `10.x` | .NET SDK version | +| `build-configuration` | ❌ | `Release` | Build configuration | +| `build-platform` | ❌ | `Any CPU` | MSBuild platform | +| `upload-build-artifacts` | ❌ | `true` | Whether to upload binary output | +| `build-artifacts-name` | ❌ | `build` | Artifact name | + +**Used by:** `build` and `pull-request` workflows. + +--- + +## source/format + +**Path:** `.github/actions/source/format` +**Description:** Runs `dotnet format whitespace`, `dotnet format style`, and optionally `dotnet format analyzers` on the codebase, then pushes any changes back to the branch via `git/push-changes`. + +| Input | Required | Default | Description | +|---|---|---|---| +| `project-path` | ✅ | — | Path or glob for the project/solution | +| `dotnet_sdk_version` | ❌ | `10.x` | .NET SDK version | +| `format-whitespace` | ❌ | `true` | Run `dotnet format whitespace` | +| `format-style` | ❌ | `true` | Run `dotnet format style` | +| `format-analyzers` | ❌ | `false` | Run `dotnet format analyzers` | +| `format-analyzers-diagnostics-parameter` | ❌ | `''` | Extra `--diagnostics` argument | + +**Used by:** `build` workflow (`format` job). + +--- + +## testing/test + +**Path:** `.github/actions/testing/test` +**Description:** Runs `dotnet test` with optional TRX logging and code coverage collection, then uploads all test result files as a workflow artifact. + +| Input | Required | Default | Description | +|---|---|---|---| +| `project-path` | ✅ | — | Glob pattern for test project files | +| `test-results-directory` | ❌ | `test-results` | Directory where test outputs are written | +| `code-coverage-settings-file` | ❌ | `''` | Path to the coverage settings XML | +| `use-trf-logger` | ❌ | `true` | Enable TRX logger (`--report-trx`) | +| `collect-code-coverage` | ❌ | `true` | Enable code coverage (`--coverage`) | +| `code-coverage-output-format` | ❌ | `cobertura` | Coverage output format | +| `upload-test-artifacts` | ❌ | `true` | Upload collected test result files | +| `test-artifacts-name` | ❌ | `test-results` | Artifact name | +| `dotnet_sdk_version` | ❌ | `10.x` | .NET SDK version | + +**Used by:** `build` and `pull-request` workflows (`test` job). + +--- + +## testing/test-report + +**Path:** `.github/actions/testing/test-report` +**Description:** Installs `LiquidTestReports.Cli` and converts `.trx` files in the test result folder into a single Markdown report. + +| Input | Required | Default | Description | +|---|---|---|---| +| `test-result-folder` | ✅ | — | Folder containing `.trx` files | +| `test-report-filename` | ❌ | `test-report.md` | Output filename | +| `dotnet_sdk_version` | ❌ | `10.x` | .NET SDK version | + +| Output | Description | +|---|---| +| `test-report-file` | Full path to the generated Markdown report | + +**Used by:** `build` and `pull-request` workflows (report written to step summary). + +--- + +## testing/code-coverage + +**Path:** `.github/actions/testing/code-coverage` +**Description:** Merges multiple Cobertura coverage files using `dotnet-coverage`, then generates a Markdown summary report with `reportgenerator`. + +| Input | Required | Default | Description | +|---|---|---|---| +| `test-result-folder` | ✅ | — | Folder containing `*.cobertura.xml` files | +| `dotnet_sdk_version` | ❌ | `10.x` | .NET SDK version | + +| Output | Description | +|---|---| +| `code-coverage-report-file` | Path to the generated `Summary.md` | +| `code-coverage-merge-file` | Path to the merged Cobertura XML file | + +**Used by:** `build` and `pull-request` workflows (report written to step summary). + +--- + +## versioning/extract-version + +**Path:** `.github/actions/versioning/extract-version` +**Description:** Extracts a `MAJOR.MINOR` version from a branch name using a configurable regex (default `(\d+).(\d+)`). Falls back to a default version if no match is found. + +| Input | Required | Default | Description | +|---|---|---|---| +| `branch-name` | ✅ | — | Branch name to parse | +| `default-version` | ❌ | `0.0` | Fallback when no version is found | +| `version-format` | ❌ | `(\d+).(\d+)` | Regex to extract the version | +| `dotnet_sdk_version` | ❌ | `10.x` | .NET SDK version | + +| Output | Description | +|---|---| +| `version` | Extracted `MAJOR.MINOR` string | + +**Used by:** All versioning-aware workflows. + +--- + +## versioning/format-version + +**Path:** `.github/actions/versioning/format-version` +**Description:** Produces all version strings used for .NET assembly metadata and NuGet package versions from a base `MAJOR.MINOR` version plus context inputs. + +| Input | Required | Description | +|---|---|---| +| `version` | ✅ | Base `MAJOR.MINOR` version | +| `patch` | ✅ | GitHub run number (used as patch segment) | +| `build-number` | ✅ | Commit count ahead of `main` | +| `sha` | ✅ | Commit SHA (appended to informational version) | +| `pre-release-tag` | ✅ | Pre-release label (`preview`, branch slug, or empty for stable) | + +| Output | Description | +|---|---| +| `friendly-version` | `MAJOR.MINOR` (human-readable label) | +| `assembly-version` | `MAJOR.MINOR.patch.buildNumber` | +| `assembly-informational-version` | `MAJOR.MINOR.patch+sha` | +| `file-version` | Same as `assembly-version` | +| `release-version` | `MAJOR.MINOR.patch[-preTag.buildNumber]` (NuGet version) | + +See [Versioning](./versioning.md) for the full pipeline. + +--- + +## Creating a New Composite Action + +1. Create a new directory under `.github/actions///`. +2. Add an `action.yml` file with `runs.using: composite`. +3. Declare all `inputs` with `description` and `required`. +4. Declare all `outputs` (if any) with `value` expressions referencing step outputs. +5. Keep each action focused on a single responsibility. +6. Reference optional `.NET SDK` version via an `inputs.dotnet_sdk_version` input (default `10.x`) for consistency with existing actions. +7. Use `actions/checkout@v6` as the first step when the action needs file access. + +```yaml +name: 'My Action' +author: 'Pete Sramek' +description: 'Short description of what this action does.' +inputs: + my-input: + description: 'Description of the input.' + required: true + dotnet_sdk_version: + description: '.NET SDK version. Default: 10.x' + required: false + default: '10.x' +outputs: + my-output: + description: 'Description of the output.' + value: ${{ steps.my-step.outputs.my-output }} +runs: + using: composite + steps: + - name: 'Checkout ${{ github.head_ref || github.ref }}' + uses: actions/checkout@v6 + - name: 'My step' + id: my-step + shell: bash + run: echo "my-output=value" >> $GITHUB_OUTPUT +``` diff --git a/docs/extensibility.md b/docs/extensibility.md new file mode 100644 index 00000000..ff8349b4 --- /dev/null +++ b/docs/extensibility.md @@ -0,0 +1,146 @@ +# Extensibility + +This guide explains how to add new coordinate types, polyline representations, and encoding schemes to PolylineAlgorithm. + +## Design Overview + +The library is built around two generic abstract base classes: + +| Class | Purpose | +|---|---| +| `AbstractPolylineEncoder` | Encodes a sequence of coordinates into an encoded polyline | +| `AbstractPolylineDecoder` | Decodes an encoded polyline into a sequence of coordinates | + +Both implement corresponding interfaces (`IPolylineEncoder` and `IPolylineDecoder`). + +Type parameters: + +| Parameter | Meaning | Examples | +|---|---|---| +| `TCoordinate` | Your coordinate type | `(double Lat, double Lon)`, a custom `GeoPoint` class | +| `TPolyline` | Your polyline representation | `string`, `char[]`, `ReadOnlyMemory`, a custom wrapper | + +## Adding a Custom Encoder + +Subclass `AbstractPolylineEncoder` and implement the three abstract methods: + +| Method | Signature | What to return | +|---|---|---| +| `GetLatitude` | `double GetLatitude(TCoordinate current)` | The latitude in decimal degrees | +| `GetLongitude` | `double GetLongitude(TCoordinate current)` | The longitude in decimal degrees | +| `CreatePolyline` | `TPolyline CreatePolyline(ReadOnlyMemory polyline)` | Your output type built from the encoded char buffer | + +Example — encode `(double Latitude, double Longitude)` tuples to `string`: + +```csharp +// +// Copyright © Pete Sramek. All rights reserved. +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace PolylineAlgorithm; + +using PolylineAlgorithm.Abstraction; + +/// +/// Encodes geographic coordinate tuples into a Google-encoded polyline string. +/// +public sealed class TuplePolylineEncoder : AbstractPolylineEncoder<(double Latitude, double Longitude), string> { + /// + protected override string CreatePolyline(ReadOnlyMemory polyline) + => polyline.ToString(); + + /// + protected override double GetLatitude((double Latitude, double Longitude) current) + => current.Latitude; + + /// + protected override double GetLongitude((double Latitude, double Longitude) current) + => current.Longitude; +} +``` + +To use custom encoding options (e.g. precision 6): + +```csharp +var options = new PolylineEncodingOptionsBuilder() + .WithPrecision(6) + .Build(); + +var encoder = new TuplePolylineEncoder(options); +``` + +## Adding a Custom Decoder + +Subclass `AbstractPolylineDecoder` and implement the two abstract methods: + +| Method | Signature | What to return | +|---|---|---| +| `GetReadOnlyMemory` | `ReadOnlyMemory GetReadOnlyMemory(in TPolyline polyline)` | A `ReadOnlyMemory` view over the encoded polyline | +| `CreateCoordinate` | `TCoordinate CreateCoordinate(double latitude, double longitude)` | An instance of your coordinate type | + +Example — decode a `string` polyline into `(double Latitude, double Longitude)` tuples: + +```csharp +// +// Copyright © Pete Sramek. All rights reserved. +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace PolylineAlgorithm; + +using PolylineAlgorithm.Abstraction; + +/// +/// Decodes a Google-encoded polyline string into geographic coordinate tuples. +/// +public sealed class TuplePolylineDecoder : AbstractPolylineDecoder { + /// + protected override ReadOnlyMemory GetReadOnlyMemory(in string polyline) + => polyline.AsMemory(); + + /// + protected override (double Latitude, double Longitude) CreateCoordinate(double latitude, double longitude) + => (latitude, longitude); +} +``` + +## Supporting a Different Polyline Format + +The encoding algorithm itself (Google's encoded polyline algorithm) is implemented in `AbstractPolylineEncoder` and `AbstractPolylineDecoder`. If you need a completely different algorithm: + +1. Create a new class in its own file — do **not** modify the existing abstract base classes. +2. Implement `IPolylineEncoder` and/or `IPolylineDecoder` directly. +3. If the new algorithm shares coordinate-type logic with an existing encoder/decoder, consider extracting that logic into a shared helper in the `PolylineAlgorithm.Utility` project. + +## Encoding Options + +`PolylineEncodingOptions` controls shared behavior. Configure it via `PolylineEncodingOptionsBuilder`: + +```csharp +var options = new PolylineEncodingOptionsBuilder() + .WithPrecision(5) // decimal digits (default: 5) + .WithLoggerFactory(myLoggerFactory) // enables internal logging + .Build(); +``` + +Pass the options to the constructor of any encoder or decoder: + +```csharp +var encoder = new TuplePolylineEncoder(options); +var decoder = new TuplePolylineDecoder(options); +``` + +## Extension Methods + +The library provides extension methods for `IPolylineEncoder` and `IPolylineDecoder` to support common collection types (`IEnumerable`, arrays, `ReadOnlyMemory`). These are in `PolylineAlgorithm.Extensions`. Your custom implementations automatically benefit from these extension methods as long as you implement the interfaces. + +## Checklist for a New Encoding Scheme + +- [ ] Create the encoder class in a new file (one class per file). +- [ ] Create the decoder class in a new file. +- [ ] Add XML doc comments to all public members. +- [ ] Add unit tests in `tests/PolylineAlgorithm.Tests/` following the [testing conventions](./testing.md). +- [ ] Add benchmarks in `benchmarks/PolylineAlgorithm.Benchmarks/` following the [benchmarking guide](./benchmarks.md). +- [ ] Update `PublicAPI.Unshipped.txt` with any new public API surface. +- [ ] Add usage samples in `samples/` if the new type is intended for end users. diff --git a/docs/local-development.md b/docs/local-development.md new file mode 100644 index 00000000..48fa5f76 --- /dev/null +++ b/docs/local-development.md @@ -0,0 +1,74 @@ +# Local Development + +This guide explains how to build, test, and format the PolylineAlgorithm codebase locally. + +## Prerequisites + +- [.NET 10 SDK](https://dotnet.microsoft.com/download) (or newer) +- A terminal / shell + +## Building + +Build the main library using the solution file: + +```bash +dotnet build PolylineAlgorithm.slnx +``` + +To build in Release configuration (required before running tests or benchmarks): + +```bash +dotnet build PolylineAlgorithm.slnx --configuration Release +``` + +## Running Tests + +Run all unit tests: + +```bash +dotnet test ./tests/PolylineAlgorithm.Tests/PolylineAlgorithm.Tests.csproj --configuration Release +``` + +> **Note:** Always use Release configuration when running tests. The Debug configuration contains a `Debug.Assert` in `AbstractPolylineEncoderTest` that will crash the test runner. + +To collect code coverage at the same time: + +```bash +dotnet test ./tests/PolylineAlgorithm.Tests/PolylineAlgorithm.Tests.csproj \ + --configuration Release \ + --coverage \ + --coverage-output-format cobertura \ + --coverage-settings ./code-coverage-settings.xml +``` + +## Running Benchmarks + +See [Benchmarks](./benchmarks.md) for full details. Quick run: + +```bash +dotnet run --project ./benchmarks/PolylineAlgorithm.Benchmarks/PolylineAlgorithm.Benchmarks.csproj \ + --configuration Release \ + --framework net10.0 \ + -- --filter '*' +``` + +## Formatting + +The project uses `dotnet format` for code style enforcement. Run all format steps before committing: + +```bash +# Fix whitespace +dotnet format whitespace + +# Fix code style +dotnet format style + +# Fix analyzer warnings (optional — run when you want to fix diagnostics) +dotnet format analyzers +``` + +The CI `format` job also runs `dotnet format` automatically on every push to non-release branches and pushes the formatted result back to the branch. + +## Editor Configuration + +Code style rules are stored in `.editorconfig` at the repository root. Any compliant IDE (Visual Studio, VS Code with C# Dev Kit, Rider) will pick these up automatically. diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 00000000..1f9ad54c --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,135 @@ +# Testing + +This guide explains the test project structure, naming conventions, and how to write new tests. + +## Project Structure + +All tests live in the `tests/` directory: + +``` +tests/ +└── PolylineAlgorithm.Tests/ + ├── Abstraction/ # Tests for abstract base classes + │ ├── AbstractPolylineDecoderTests.cs + │ └── AbstractPolylineEncoderTests.cs + ├── Internal/ # Tests for internal helpers + │ ├── CoordinateDeltaTests.cs + │ ├── Pow10Tests.cs + │ └── Diagnostics/ + ├── Extensions/ # Tests for extension methods + ├── InvalidPolylineExceptionTests.cs + ├── PolylineEncodingTests.cs + ├── PolylineEncodingOptionsBuilderTests.cs + └── PolylineAlgorithm.Tests.csproj +``` + +## Test Framework + +Tests use **MSTest** with `Microsoft.Testing.Platform` runner. The project targets `net8.0`, `net9.0`, and `net10.0`. + +Key NuGet packages: + +| Package | Purpose | +|---|---| +| `MSTest` | Test attributes and assertions | +| `Microsoft.NET.Test.Sdk` | Test runner integration | +| `Microsoft.Testing.Extensions.CodeCoverage` | Code coverage collection | +| `Microsoft.Testing.Extensions.TrxReport` | TRX report generation | +| `Microsoft.Extensions.Diagnostics.Testing` | Logging test helpers | + +## Naming Conventions + +Follow the existing pattern: `{Subject}_{Scenario}_{ExpectedResult}`. + +Examples: + +``` +Decode_With_Null_Polyline_Throws_ArgumentNullException +Normalize_ZeroValue_ReturnsZero +Normalize_With_Value_And_Precision_Returns_Expected_Normalized_Value +``` + +## Writing a New Test Class + +1. Create a new `.cs` file in the appropriate subdirectory of `tests/PolylineAlgorithm.Tests/`. +2. Use the `[TestClass]` attribute and mark it `sealed`. +3. Add the standard copyright header (copy from an existing file). +4. Annotate every public test method with `[TestMethod]` and an XML doc comment. + +Example structure: + +```csharp +// +// Copyright © Pete Sramek. All rights reserved. +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace PolylineAlgorithm.Tests; + +/// +/// Tests for . +/// +[TestClass] +public sealed class MyClassTests { + /// + /// Tests that returns the expected result. + /// + [TestMethod] + public void MyMethod_WithValidInput_ReturnsExpected() { + // Arrange + var sut = new MyClass(); + + // Act + var result = sut.MyMethod("input"); + + // Assert + Assert.AreEqual("expected", result); + } +} +``` + +## Data-Driven Tests + +Use `[DataRow]` to test multiple cases in a single method: + +```csharp +[TestMethod] +[DataRow(37.78903, 5u, 3778903)] +[DataRow(-122.4123, 5u, -12241230)] +public void Normalize_With_Value_And_Precision_Returns_Expected(double value, uint precision, int expected) { + int result = PolylineEncoding.Normalize(value, precision); + Assert.AreEqual(expected, result); +} +``` + +## Testing Abstract Base Classes + +Internal test implementations are defined as private sealed classes inside the test class. This avoids polluting the public namespace: + +```csharp +[TestClass] +public sealed class AbstractPolylineDecoderTests { + private sealed class TestStringDecoder : AbstractPolylineDecoder { + protected override ReadOnlyMemory GetReadOnlyMemory(in string polyline) => polyline.AsMemory(); + protected override (double, double) CreateCoordinate(double lat, double lon) => (lat, lon); + } + + [TestMethod] + public void Decode_With_Null_Polyline_Throws_ArgumentNullException() { + var decoder = new TestStringDecoder(); + Assert.ThrowsExactly(() => decoder.Decode((string?)null!).ToList()); + } +} +``` + +## Running Tests Locally + +```bash +dotnet test ./tests/PolylineAlgorithm.Tests/PolylineAlgorithm.Tests.csproj --configuration Release +``` + +Always use `--configuration Release`. See [Local Development](./local-development.md) for more options including code coverage. + +## Code Coverage + +Coverage is configured via `code-coverage-settings.xml` at the repository root. The CI pipeline uses `dotnet-coverage` to merge multiple coverage files and `reportgenerator` to produce a Markdown summary posted to the workflow step summary. diff --git a/docs/versioning.md b/docs/versioning.md new file mode 100644 index 00000000..7db7a79e --- /dev/null +++ b/docs/versioning.md @@ -0,0 +1,99 @@ +# Versioning + +This document explains how version numbers are derived from branch names and how they flow through the build pipeline. + +## Branch Naming Convention + +The version is embedded in the branch name for `preview/**` and `release/**` branches: + +| Branch | Example | Extracted version | +|---|---|---| +| `preview/X.Y` | `preview/1.2` | `1.2` | +| `release/X.Y` | `release/1.2` | `1.2` | +| Any other branch | `develop/my-feature` | `0.0` (default fallback) | + +The regex used to extract the version is `(\d+).(\d+)` (configurable via the `version-format` input of `versioning/extract-version`). + +## Version Pipeline + +Each versioning-aware workflow runs two composite actions in sequence: + +``` +Branch name + │ + ▼ +[versioning/extract-version] + │ output: version = "X.Y" + ▼ +[versioning/format-version] + │ inputs: version, patch (run number), build-number, sha, pre-release-tag + ▼ + outputs: friendly-version, assembly-version, assembly-informational-version, + file-version, release-version +``` + +### Step 1 — Extract version (`versioning/extract-version`) + +Applies the regex against the branch name. Falls back to `0.0` for non-versioned branches. + +### Step 2 — Determine pre-release tag + +Before calling `format-version`, the workflow computes a pre-release tag: + +| Branch type | Pre-release tag | +|---|---| +| `release/**` | _(empty — stable release)_ | +| `preview/**` | `preview` | +| Any other | Branch name slug (e.g. `develop-my-feature`) | + +### Step 3 — Format version (`versioning/format-version`) + +Uses `version`, `patch` (= `github.run_number`), `build-number` (commits ahead of `main`), `sha`, and `pre-release-tag` to produce: + +| Output | Formula | Example | +|---|---|---| +| `friendly-version` | `X.Y` | `1.2` | +| `assembly-version` | `X.Y.patch.buildNumber` | `1.2.42.7` | +| `assembly-informational-version` | `X.Y.patch+sha` | `1.2.42+abc1234` | +| `file-version` | `X.Y.patch.buildNumber` | `1.2.42.7` | +| `release-version` (stable) | `X.Y.patch.buildNumber` | `1.2.42.7` | +| `release-version` (pre-release) | `X.Y.patch-preTag.buildNumber` | `1.2.42-preview.7` | + +## Where Versions Are Used + +| Version string | Used as | +|---|---| +| `assembly-version` | `/p:Version` and `/p:AssemblyVersion` in `dotnet build` / `dotnet pack` | +| `assembly-informational-version` | `/p:AssemblyInformationalVersion` in `dotnet build` | +| `file-version` | `/p:FileVersion` in `dotnet build` | +| `release-version` | `/p:PackageVersion` in `dotnet pack` (the NuGet package version) | +| `friendly-version` | Human-readable label in release names, artifact names, and documentation paths | + +## Build Number Calculation + +`build-number` counts the commits on the current branch that are not on `main`: + +```bash +git fetch --unshallow --filter=tree:0 +build_number=$(git rev-list --count origin/ ^origin/main) +``` + +This means two builds from the same branch will produce different `assembly-version` and `release-version` strings only if new commits are added. + +## Examples + +Given branch `preview/1.2`, run number `55`, 3 commits ahead of `main`, and SHA `abc1234f`: + +| Output | Value | +|---|---| +| `friendly-version` | `1.2` | +| `assembly-version` | `1.2.55.3` | +| `assembly-informational-version` | `1.2.55+abc1234f` | +| `file-version` | `1.2.55.3` | +| `release-version` | `1.2.55-preview.3` | + +Given branch `release/1.2`, same inputs: + +| Output | Value | +|---|---| +| `release-version` | `1.2.55.3` _(no pre-release tag)_ | diff --git a/docs/workflows.md b/docs/workflows.md new file mode 100644 index 00000000..4c864783 --- /dev/null +++ b/docs/workflows.md @@ -0,0 +1,178 @@ +# Workflows + +This document describes all six CI/CD workflows and explains how they connect. + +## Overview + +``` +Feature branch push + │ + ▼ + [build.yml] ──── format → compile → test → pack → publish-dev + assembly metadata + │ + │ promote-branch (manual) + ▼ + preview/X.Y branch + │ + ▼ + [pull-request.yml] ──── compile → test → pack → publish-dev + benchmark (optional) + │ + │ merge PR + ▼ + [release.yml] ──── compile → test → pack → publish-NuGet → GitHub Release → documentation + │ + │ publish-documentation (manual) + ▼ + GitHub Pages (petesramek.github.io/polyline-algorithm-csharp) +``` + +Version bumping runs independently via `bump-version.yml` (manual trigger). + +--- + +## build.yml + +**Trigger:** Push to any branch except `preview/**` and `release/**`, when files under `src/` change. + +**Jobs (in order):** + +| Job | Depends on | Description | +|---|---|---| +| `workflow-variables` | — | Sets `is-release` and `is-preview` flags | +| `versioning` | — | Extracts version from branch name, builds semver strings | +| `format` | — | Runs `dotnet format` and pushes changes back to the branch | +| `build` | `versioning`, `format` | Compiles source, uploads `build` artifact | +| `test` | `build` | Runs MSTest suite, generates test + coverage reports | +| `pack` | `versioning`, `build` | Creates `.nupkg` / `.snupkg`, uploads `package` artifact | +| `publish-package` | `pack` | Pushes package to Azure Artifacts (Development environment) | +| `generate-assembly-metadata` | `versioning`, `build` | Runs `docfx metadata`, commits versioned YAML to `api-reference/` | + +**Purpose:** Continuous validation of feature branches. Also keeps the `api-reference/` directory up-to-date with every successful build. + +--- + +## pull-request.yml + +**Trigger:** Pull requests targeting `preview/**` or `release/**` branches (`opened`, `synchronize`, `reopened`). + +**Jobs (in order):** + +| Job | Depends on | Description | +|---|---|---| +| `workflow-variables` | — | Sets `is-release` and `is-preview` flags | +| `versioning` | `workflow-variables` | Extracts version from **base** branch | +| `build` | `versioning` | Compiles source | +| `test` | `build` | Runs tests, generates reports | +| `pack` | `versioning`, `build` | Packages binaries | +| `publish-development-package` | `pack` | Pushes to Azure Artifacts (Development environment) | +| `benchmark` | `build` | BenchmarkDotNet run on Ubuntu, Windows, macOS — only when `BENCHMARKDOTNET_RUN_OVERRIDE=true` or building a release branch | + +**Purpose:** Validates the change before it merges into a stabilization branch. Benchmark results are uploaded as per-OS artifacts and appended to the step summary. + +--- + +## release.yml + +**Trigger:** Push to `preview/**` or `release/**` branches when files under `src/` change (i.e., after a PR is merged). + +**Jobs (in order):** + +| Job | Depends on | Description | +|---|---|---| +| `workflow-variables` | — | Sets `is-release` / `is-preview` flags | +| `validate-release` | `workflow-variables` | Fails fast if the branch is neither `preview/**` nor `release/**` | +| `versioning` | `workflow-variables`, `validate-release` | Extracts and formats version from the branch name | +| `build` | `versioning` | Compiles source | +| `test` | `build` | Runs tests | +| `pack` | `versioning`, `build` | Packages binaries | +| `publish-package` | `pack` | Publishes to NuGet.org (Production environment) | +| `create-release` | `versioning`, `publish-package` | Creates a git tag + GitHub release with auto-generated notes | +| `generate-docs` | `versioning` | Builds DocFX site | +| `publish-docs` | `generate-docs` | Deploys to GitHub Pages | + +**Purpose:** Full release pipeline. Triggered by merging a PR into `preview/**` (pre-release) or `release/**` (stable release). + +--- + +## bump-version.yml + +**Trigger:** Manual (`workflow_dispatch`). + +**Inputs:** + +| Input | Values | Description | +|---|---|---| +| `bump-type` | `minor` / `major` | Type of version bump | + +**What it does:** + +1. Reads the current version from the default branch. +2. Increments the `MINOR` or `MAJOR` component. +3. Unlocks the target branches (via `branch-protection/unlock`), commits the new version number, and re-locks them. +4. Creates a pull request for the version bump change. + +**Purpose:** Controlled version increment without manual editing of version files. + +--- + +## promote-branch.yml + +**Trigger:** Manual (`workflow_dispatch`). + +**Inputs:** + +| Input | Values | Description | +|---|---|---| +| `promotion-type` | `preview` / `release` | Target stabilization tier | +| `base-branch` | string | Branch to branch off from (default: `main`) | + +**Validation rules:** + +- `preview` promotion requires source to be a `develop/**` or `support/**` branch. +- `release` promotion requires source to be a `preview/**` branch. +- Source and target branches must be different. +- An open PR from source → target must not already exist. + +**What it does:** + +1. Extracts the version from the current branch name. +2. Derives the target branch name (`preview/X.Y` or `release/X.Y`). +3. Creates the target branch if it does not exist, then locks it. +4. Opens a pull request from the current branch into the target branch. + +**Purpose:** Promotes code from a development branch into a stabilization branch following the [branch strategy](./branch-strategy.md). + +--- + +## publish-documentation.yml + +**Trigger:** Manual (`workflow_dispatch`). + +**Jobs:** + +| Job | Description | +|---|---| +| `workflow-variables` | Captures `github.run_number` | +| `versioning` | Extracts version from the current branch | +| `generate-docs` | Runs `docfx build` from `api-reference/api-reference.json`, uploads result to `api-reference/_docs/`, then to `github-pages` artifact | +| `publish-docs` | Deploys `github-pages` artifact to GitHub Pages | + +**Purpose:** Re-publishes the documentation site on demand, without needing a code change. Useful after updating guide articles or fixing doc rendering issues. + +--- + +## Shared Environment Variables + +All workflows share these top-level `env` defaults: + +| Variable | Value | +|---|---| +| `dotnet-sdk-version` | `10.x` | +| `build-configuration` | `Release` | +| `build-platform` | `Any CPU` | +| `test-result-directory` | `test-results` | +| `nuget-packages-directory` | `nuget-packages` | + +## Concurrency + +Each workflow uses a `concurrency` group keyed on the branch or ref to prevent parallel runs from conflicting. Most use `cancel-in-progress: false` to avoid canceling in-flight releases; only `pull-request` uses `cancel-in-progress: true` to discard stale runs quickly.