Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/workflows/publish-documentation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

on:
workflow_dispatch:
push:
branches:
- 'release/**'
paths:
- 'src/**'
- 'api-reference/**'

permissions:
actions: read
Expand All @@ -13,8 +19,20 @@ concurrency:
cancel-in-progress: true

jobs:
validate-branch:
name: 'Validate branch'
runs-on: ubuntu-latest
steps:
- name: 'Ensure documentation is published from a release branch'
if: ${{ !startsWith(github.ref_name, 'release/') }}
run: |
echo "Documentation should only be published from 'release/**' branches."
echo "Current branch: '${{ github.ref_name }}'"
exit 1

workflow-variables:
name: 'Workflow variables'
needs: [validate-branch]
runs-on: ubuntu-latest

outputs:
Expand Down
52 changes: 52 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ permissions:
pages: write
id-token: write
contents: write
administration: write

concurrency:
group: release-${{ github.head_ref || github.ref }}
Expand Down Expand Up @@ -314,3 +315,54 @@ jobs:
else
echo "⏭️ Skipped merge to main: **${{ env.current-branch }}** is not the highest release branch." >> $GITHUB_STEP_SUMMARY
fi

create-support-branch:
name: 'Create support branch for ${{ github.ref_name }}'
needs: [workflow-variables, release, versioning]
if: ${{ needs.workflow-variables.outputs.is-release == 'true' }}
runs-on: ubuntu-latest
env:
current-version: ${{ needs.versioning.outputs.friendly-version }}
steps:
- name: 'Checkout ${{ github.head_ref || github.ref }}'
uses: actions/checkout@v6
with:
fetch-depth: 0

- name: 'Resolve support branch name'
id: resolve-support-branch
run: |
major_minor=$(echo "${{ env.current-version }}" | grep -oP '^\d+\.\d+')
echo "support-branch=support/$major_minor" >> $GITHUB_OUTPUT

- name: 'Check if support branch already exists'
id: check-support-branch
run: |
git fetch origin
if git ls-remote --exit-code --heads origin "${{ steps.resolve-support-branch.outputs.support-branch }}" > /dev/null 2>&1; then
echo "support-branch-exists=true" >> $GITHUB_OUTPUT
else
echo "support-branch-exists=false" >> $GITHUB_OUTPUT
fi

- name: 'Create support branch'
if: ${{ steps.check-support-branch.outputs.support-branch-exists == 'false' }}
run: |
git config user.name "$(git log -n 1 --pretty=format:%an)"
git config user.email "$(git log -n 1 --pretty=format:%ae)"
git checkout -b "${{ steps.resolve-support-branch.outputs.support-branch }}"
git push --set-upstream origin "${{ steps.resolve-support-branch.outputs.support-branch }}"

- name: 'Lock support branch'
if: ${{ steps.check-support-branch.outputs.support-branch-exists == 'false' }}
uses: './.github/actions/github/branch-protection/lock'
with:
branch: ${{ steps.resolve-support-branch.outputs.support-branch }}

- name: 'Write support branch summary'
run: |
if [[ "${{ steps.check-support-branch.outputs.support-branch-exists }}" == "false" ]]; then
echo "✅ Created and locked support branch **${{ steps.resolve-support-branch.outputs.support-branch }}**." >> $GITHUB_STEP_SUMMARY
else
echo "⏭️ Support branch **${{ steps.resolve-support-branch.outputs.support-branch }}** already exists." >> $GITHUB_STEP_SUMMARY
fi
1 change: 0 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ In-depth developer guides are in the [`/docs`](./docs/README.md) folder:
- [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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,8 @@
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<Compile Remove="C:\Users\petesramek\.nuget\packages\microsoft.visualstudio.diagnosticshub.benchmarkdotnetdiagnosers\18.3.36812.1\contentFiles\cs\any\BenchmarkProfilerAgentConfig.g.cs" />
<Compile Remove="C:\Users\petesramek\.nuget\packages\microsoft.visualstudio.diagnosticshub.benchmarkdotnetdiagnosers\18.6.37110.2\contentFiles\cs\any\BenchmarkProfilerAgentConfig.g.cs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.15.8" />
<PackageReference Include="BenchmarkDotNet.Diagnostics.Windows" Version="0.15.8" />
<PackageReference Include="Microsoft.VisualStudio.DiagnosticsHub.BenchmarkDotNetDiagnosers" Version="18.6.37110.2" />
</ItemGroup>

<ItemGroup>
Expand Down
1 change: 0 additions & 1 deletion docs/benchmarks.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ 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

Expand Down
62 changes: 44 additions & 18 deletions docs/branch-strategy.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,56 +7,80 @@ This document describes the branch model, the purpose of each branch type, and h
| Pattern | Purpose | Protected |
|---|---|---|
| `main` | Latest stable source of truth | ✅ Yes |
| `develop/**` | Active feature development | ❌ No |
| `support/**` | Maintenance / backport development | ❌ No |
| `develop/X.Y` | Active feature development sink for version X.Y | ✅ Yes (PR only) |
| `support/X.Y` | Maintenance / backport development sink for version X.Y | ✅ Yes (PR only) |
| `feature/<id>-<description>` | Individual feature work, merged into `develop/X.Y` via PR | ❌ No |
| `bugfix/<id>-<description>` | Bug fix work, merged into `support/X.Y` via PR | ❌ 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)
└─ feature/123-my-feature
push to src/ → [build.yml] runs: format, compile, test, pack, publish-dev
PR → develop/X.Y
2. Promote to preview
└─ promote-branch.yml (manual) → creates preview/X.Y + PR: develop → preview/X.Y
2. Bug fix work
└─ bugfix/124-my-fix
│ PR → support/X.Y
3. Promote to preview
└─ promote-branch.yml (manual) → creates preview/X.Y + PR: develop/X.Y → 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
4. 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
│ PR merged → [release.yml]: publish-NuGet (stable), GitHub release, docs, creates support/X.Y (first time only)
4. Back-merge (optional)
└─ Manual PR: release/X.Y → main
5. Back-merge to main (automatic, highest version only)
└─ [release.yml] creates PR: release/X.Y → main (only when X.Y is the highest release branch)
│ PR merged → main is updated to the latest stable source
```

## Rules Per Branch Type

### `main`

- Represents the current stable release.
- Represents the latest stable release — always in sync with the highest released version.
- Direct pushes are not allowed (protected).
- Updated by merging from `release/X.Y` after a stable release.
- Updated automatically via a PR created by `release.yml` whenever the highest `release/X.Y` branch publishes a stable release.
- Serves as the baseline for version bumps: new development versions are derived from the state of `main` at the point the previous release left off.
- 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/**`
### `develop/X.Y`

- Naming convention: `develop/<description>` (e.g. `develop/async-decoder`, `develop/1.2`).
- Naming convention: `develop/<major>.<minor>` (e.g. `develop/1.2`).
- Protected: all changes are merged via pull request from `feature/**` branches.
- 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/**`
### `support/X.Y`

- Used for backport and maintenance work against older versions.
- Same CI behavior as `develop/**`.
- Naming convention: `support/<major>.<minor>` (e.g. `support/1.0`).
- Auto-created when the first stable release from `release/X.Y` is published.
- Protected: all changes are merged via pull request from `bugfix/**` branches.
- Can be promoted to `preview/X.Y` for a patch release.

### `feature/<id>-<description>`

- Short-lived branch for individual feature work (e.g. `feature/123-async-decoder`).
- Merged into the appropriate `develop/X.Y` via pull request.
- Not protected — deleted after merging.

### `bugfix/<id>-<description>`

- Short-lived branch for bug fixes (e.g. `bugfix/124-decode-overflow`).
- Merged into the appropriate `support/X.Y` via pull request.
- Not protected — deleted after merging.

### `preview/X.Y`

- Created automatically by `promote-branch.yml`.
Expand All @@ -70,6 +94,8 @@ This document describes the branch model, the purpose of each branch type, and h
- 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.
- After the first stable release, a corresponding `support/X.Y` branch is auto-created.
- When `X.Y` is the highest release branch, `release.yml` automatically opens a PR to merge back into `main`, keeping `main` in sync with the latest stable state.

## Version in Branch Names

Expand All @@ -84,4 +110,4 @@ The `X.Y` in `preview/X.Y` and `release/X.Y` drives the version pipeline. See [V

## 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.
`preview/**` and `release/**` branches are locked via the [`github/branch-protection/lock`](./composite-actions.md#githubbranch-protectionlock) composite action when created. `develop/X.Y` and `support/X.Y` branches must be manually configured as protected in repository settings (PR required, no direct pushes). 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.
20 changes: 1 addition & 19 deletions docs/extensibility.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Extensibility

This guide explains how to add new coordinate types, polyline representations, and encoding schemes to PolylineAlgorithm.
This guide explains how to use PolylineAlgorithm with your own coordinate types and polyline representations.

## Design Overview

Expand Down Expand Up @@ -105,14 +105,6 @@ public sealed class TuplePolylineDecoder : AbstractPolylineDecoder<string, (doub
}
```

## 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<TCoordinate, TPolyline>` and/or `IPolylineDecoder<TPolyline, TCoordinate>` 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`:
Expand All @@ -134,13 +126,3 @@ var decoder = new TuplePolylineDecoder(options);
## Extension Methods

The library provides extension methods for `IPolylineEncoder` and `IPolylineDecoder` to support common collection types (`IEnumerable<T>`, arrays, `ReadOnlyMemory<T>`). 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.
6 changes: 3 additions & 3 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,13 @@ Key NuGet packages:

## Naming Conventions

Follow the existing pattern: `{Subject}_{Scenario}_{ExpectedResult}`.
Follow the pattern: `{Subject}_{With_Context}_{Expected_Result}` — every word separated by an underscore.

Examples:

```
Decode_With_Null_Polyline_Throws_ArgumentNullException
Normalize_ZeroValue_ReturnsZero
Normalize_With_Zero_Value_Returns_Zero
Normalize_With_Value_And_Precision_Returns_Expected_Normalized_Value
```

Expand Down Expand Up @@ -75,7 +75,7 @@ public sealed class MyClassTests {
/// Tests that <see cref="MyClass.MyMethod"/> returns the expected result.
/// </summary>
[TestMethod]
public void MyMethod_WithValidInput_ReturnsExpected() {
public void MyMethod_With_Valid_Input_Returns_Expected() {
// Arrange
var sut = new MyClass();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public void Decode_With_Invalid_Character_Polyline_Throws_InvalidPolylineExcepti
/// Tests that Decode with a valid polyline returns the expected coordinates.
/// </summary>
[TestMethod]
public void Decode_ValidPolyline_ReturnsExpectedCoordinates() {
public void Decode_With_Valid_Polyline_Returns_Expected_Coordinates() {
// Arrange
TestStringDecoder decoder = new();
string polyline = StaticValueProvider.Valid.GetPolyline();
Expand All @@ -91,7 +91,7 @@ public void Decode_ValidPolyline_ReturnsExpectedCoordinates() {
/// Tests that the options constructor with null throws <see cref="ArgumentNullException"/>.
/// </summary>
[TestMethod]
public void Constructor_WithNullOptions_ThrowsArgumentNullException() {
public void Constructor_With_Null_Options_Throws_ArgumentNullException() {
// Act & Assert
ArgumentNullException ex = Assert.ThrowsExactly<ArgumentNullException>(() => new TestStringDecoderWithOptions(null!));
Assert.AreEqual("options", ex.ParamName);
Expand All @@ -101,7 +101,7 @@ public void Constructor_WithNullOptions_ThrowsArgumentNullException() {
/// Tests that the Options property returns the configured options.
/// </summary>
[TestMethod]
public void Options_Default_ReturnsDefaultOptions() {
public void Options_With_Default_Returns_Default_Options() {
// Arrange
TestStringDecoder decoder = new();

Expand All @@ -114,7 +114,7 @@ public void Options_Default_ReturnsDefaultOptions() {
/// Tests that the options constructor stores the provided options.
/// </summary>
[TestMethod]
public void Constructor_WithOptions_StoresOptions() {
public void Constructor_With_Options_Stores_Options() {
// Arrange
PolylineEncodingOptions options = PolylineEncodingOptionsBuilder.Create()
.WithPrecision(7)
Expand All @@ -131,7 +131,7 @@ public void Constructor_WithOptions_StoresOptions() {
/// Tests that Decode with a pre-cancelled token throws <see cref="OperationCanceledException"/>.
/// </summary>
[TestMethod]
public void Decode_PreCancelledToken_ThrowsOperationCanceledException() {
public void Decode_With_Pre_Cancelled_Token_Throws_OperationCanceledException() {
// Arrange
TestStringDecoder decoder = new();
string polyline = StaticValueProvider.Valid.GetPolyline();
Expand Down
Loading
Loading