Keegan Ott / dreekoSlop

Fork maintenance · · By · 5 min read

Maintain a DurableTask SQL fork without owning it forever

Forking the provider solved an immediate self-hosted-edition problem. The harder question was how to keep taking upstream fixes without turning one patch into a permanent private runtime.

Two source-control tracks merging under a magnifying glass beside a patch ledger
A private change is manageable only when its reason, test, upstream path and deletion condition stay visible.

Put these three files in the root of the fork

  1. Write the ledger row and regression test before changing the provider.
  2. Review the scripts, mark them executable, and add the upstream remote.
  3. For every upstream tag, run ./upstream-integration.sh vX.Y.Z.
  4. After resolving the merge, run ./verify-fork.sh vX.Y.Z and attach its output to the release record.

The defaults match the upstream project layout. If the fork has moved projects, set PACKAGE_PROJECT and TEST_PROJECT; the verifier exits rather than quietly testing the wrong thing.

A fork is cheap on the day it is created. The invoice arrives with the next upstream release, security advisory, SQL migration or transitive dependency conflict. My rule became: every difference from upstream needs a reason, a regression test and a condition under which it can be deleted.

Make every patch make a falsifiable claim

| Patch | Reason | Test | Upstream | Delete when |
|------|--------|------|----------|-------------|
| P001 | isolated multi-app Task Hubs | MultiHubIsolation | issue/PR URL | released + verified |
| P002 | dependency compatibility | PackageGraph | issue/PR URL | product moves baseline |

The wording matters. “Self-hosted changes” is not a reason; nobody can review it six months later. Describe the broken behaviour, affected versions and why configuration or an adapter could not solve it outside the fork.

Make the package identify its source

Do not publish a private build with the same identity as upstream. Give it an unambiguous package version and record the upstream commit:

Microsoft.DurableTask.SqlServer
1.6.0-company.4
upstream: microsoft/durabletask-mssql@<full commit>
source-date-epoch: <commit timestamp>
sha256: <package digest>

Commit the lock file and central package versions. Build in a clean container against the same internal NuGet snapshot used for the disconnected release. A package that can only be rebuilt while nuget.org is reachable is not part of an offline product.

Run dotnet list package --include-transitive --vulnerable on the finished package graph, not only the fork's direct references. While validating the reconstructed migrator for this article, the current stable provider graph raised NU1904 for an old transitive System.Drawing.Common. Whether that assembly is reachable in your runtime is a separate assessment; silently inheriting it is not.

Make the regression test reproduce the original bug first

A green upstream suite is necessary but not enough. The product-specific suite should stay narrow:

If a private test does not explain a private patch, either the test is noise or the patch is undocumented.

Merge tagged upstream releases on a disposable branch

I would not merge upstream main directly into the production fork. For each tagged release:

  1. Run the integration script from a clean checkout; it refuses local changes.
  2. Merge the upstream tag without squashing away provenance.
  3. Review src/DurableTask.SqlServer/Scripts, SqlDbManager.cs, Directory.Packages.props, and the patch ledger before reading the rest of the diff.
  4. Run upstream tests, then the product-specific suite that proves each ledger row.
  5. Run the verifier and retain its package digest and vulnerability output.
  6. Promote only after an upgrade test from the currently shipped version.

A regular merge is less dramatic than a two-year rebase. It also makes the real cost of each private patch visible while the people who remember it are still around.

Assume schema changes are code changes

The provider assembly contains ordered SQL scripts. A source merge can therefore alter the database even when the C# diff looks harmless. Review src/DurableTask.SqlServer/Scripts, the reported extension version and SqlDbManager together. Package, DLL and schema version belong in one release record.

Give every patch an exit

When upstream ships an equivalent fix, remove the private patch on the integration branch and run its regression test unchanged. Do not keep both implementations “for safety”. If the test passes, the upstream code has earned the right to replace ours. If it fails, the difference is now concrete enough to report upstream.

Some product behaviour should never have entered the fork. Task Hub naming policy, deployment inventory and application-specific telemetry belong outside it. The less the provider knows about the product, the easier it is to delete the fork later.

I do not think carrying a fork is automatically reckless. Carrying an unexplained fork is. The useful measure is not the number of changed lines; it is whether each divergence is still testable, attributable and removable.

References: upstream durabletask-mssql, its embedded schema design, and build and test guidance.

← Start with the Task Hub isolation failure