Skip to content

Rebrand the Polly fork as Fences - #2

Merged
iancooper merged 20 commits into
mainfrom
fork/rebrand-to-fences
Aug 25, 2026
Merged

iancooper merged 20 commits into
mainfrom
fork/rebrand-to-fences

Conversation

@iancooper

Copy link
Copy Markdown
Member

Rebrands the detached Polly 8.7.0 fork as Fences and makes it an independent project. Seventeen commits, phases 0–7 of fork-migration-plan.md.

This is the first time any of this work has run through CI, so the point of the pull request is as much to exercise the rewritten build as to review the diff.

What this does

  • Legal and attribution. NOTICE.md, ACKNOWLEDGEMENTS.md and ADR 0002 record provenance. LICENSE retains App vNext's copyright alongside ours, as BSD-3-Clause requires. CHANGELOG.md is deliberately untouched — it is the historical record of upstream.
  • The rename. Polly.* → Paramore.Fences.* across 885 files: namespaces, assemblies, package IDs, project and folder names, telemetry (resilience.polly.* → resilience.fences.*). A new Fences.snk; Polly.snk is gone. Exactly one public type is renamed, PollyServiceCollectionExtensions → ResilienceServiceCollectionExtensions.
  • Governance. CONTRIBUTING.md, CODE_OF_CONDUCT.md, SECURITY.md, CLA.txt, CODEOWNERS, issue forms.
  • Agentic workflow support. .agent_instructions/, AGENTS.md, CLAUDE.md, .claude/commands/.
  • CI. Signing dropped, Codecov dropped, three dead workflows deleted, and the Polly bot secrets replaced with GITHUB_TOKEN. publish-nuget is armed and tag-gated.
  • Documentation. The prose rewrite, plus 31 broken xref: cross-references, a broken toc entry, and telemetry docs that described names the code no longer emits.

What is deliberately still Polly

Not misses:

  • Microsoft.Extensions.Http.Resilience has a hard dependency on Polly, and hands its callback a Polly ResiliencePipelineBuilder. Fences cannot remove Polly from an application that calls AddStandardResilienceHandler. docs/community/http-client-integrations.md and samples/Chaos use Polly's API on purpose and say so.
  • docs/migration-v8.md and docs/v7/extensibility.md are Polly's v7→v8 material, reframed as API generations — Fences ships the pre-v8 API as Paramore.Fences, but there was never a Fences v7.
  • Upstream citations: App-vNext/Polly issues and wiki, Polly-Contrib, Polly.Contrib.WaitAndRetry.
  • CHANGELOG.md describes Polly releases up to the fork point.

Verification so far

  • 3044 passed / 1 skipped on each of net8.0, net9.0 and net10.0, matching the pre-rename baseline exactly.
  • Public API diff is the single documented type rename and nothing else.
  • docfx metadata and docfx build both exit 0 with 0 warnings from a clean _site.
  • dotnet mdsnippets --validate-content clean; markdownlint over CI's exact glob set clean on every tracked file.
  • Not runnable locally: actionlint, zizmor, and the aspell spellcheck. All 13 workflows were YAML-parsed instead.

After merge

Fences publishes pre-releases — 9.0.0-alpha001 onwards — to secure the package identifiers and gather feedback. README.md and package-readme.md both state that the fork may be retired if App vNext reverses the Open Source Maintainers Fee, and that migrating back is the same change in reverse.

🤖 Generated with Claude Code

https://claude.ai/code/session_014HCSZBA6HSzikr1XTg7bPp

iancooper and others added 17 commits August 22, 2026 14:15
Fences is a detached copy of App-vNext/Polly at 47e3b41 (Polly 8.7.0). This
lands the working documents for turning it into an independent project.

- fork-migration-plan.md: the authoritative plan. Decisions D1-D11 (all
  confirmed), phases 0-8, risks, and the decision log.
- CLAUDE.md: an interim stub pointing a fresh agent session at the plan.
  Phase 5 replaces it with the full version.
- assets/logo/: the accepted Fences mark and lockup, as SVG source.
- images/: PNG exports at 64, 128 and 264, plus the 512 lockup.
- .gitignore: PROMPT.md (local session state) and baseline/ (the pre-rename
  build and public-API baselines, which are a local oracle, not repo content).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014HCSZBA6HSzikr1XTg7bPp
global.json pins 10.0.400, which is correct and stays: it is the current
latest .NET 10 SDK. The problem is that an exact pin with no rollForward
blocks any contributor on 10.0.1xx/2xx/3xx outright, which is how this
machine failed before the SDK was installed side-by-side.

rollForward: latestFeature accepts any 10.0.x installed at or above the pin.
This matters more now that D-level decisions dropped update-dotnet-sdk.yml
(Phase 6): nothing bumps the pin automatically any more, so the pin needs to
be tolerant rather than exact.

Refs P0.1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014HCSZBA6HSzikr1XTg7bPp
Do this before the rename, so the record of what we forked and why is
committed while the tree still visibly matches upstream.

Fences derives from App-vNext/Polly at 47e3b41 (Polly 8.7.0). The code is
BSD-3-Clause, which permits redistribution provided the copyright notice and
disclaimer are retained, and requires that the upstream names not be used to
endorse or promote a derived product. This commit is the machinery for both
halves of that: credit what is theirs, and be unambiguous that Fences is not
theirs.

- LICENSE: our copyright line added alongside App vNext's, which stays
  verbatim. The terms are unchanged; BSD-3-Clause stays.
- NOTICE.md: what Fences is, the exact upstream commit it derives from, the
  retained notice, and an explicit statement that we are not affiliated with,
  endorsed by, or supported by App vNext or the Polly maintainers.
- ACKNOWLEDGEMENTS.md: App vNext, Michael Wolfenden, the maintainer team and
  the contributor community. Polly is actively maintained and remains the
  right choice for many users; Fences is a divergence, not a criticism.
- CHANGELOG.md: upstream's 92 KB of history kept intact, headed by a note that
  8.7.0 and below are Polly's releases, not ours, and that the App-vNext links
  in them are historical citations. Fences entries start at 9.0.0.
- docs/adr/0001: adopt ADRs, matching Brighter's convention and format.
- docs/adr/0002: the fork decision itself, carrying D1-D11 and the attribution
  guardrails G1-G6 into a durable home. The migration plan is retired once the
  migration completes; this is not.

Polly-owned imagery is removed, per G4 - trade dress is separate from the
copyright licence:

- Polly-Logo.{jpg,png} and all of logos/ deleted. That includes aws.png,
  github.png and microsoft.png: nothing referenced them, they were assets for
  a sponsor section that does not survive, and they are third-party marks we
  have no reason to carry.
- package-icon.png and docs/icon.png replaced with the Fences mark at 128x128.
  eng/Library.targets packs package-icon.png unconditionally, so this was a
  hard blocker for packing; a full pack has been run against the new icon.
- package-readme.md and docs/index.md: the remote Polly logo images swapped for
  the Fences mark. Only the images - the prose in both files is still Polly's
  and is left for Phases 2-3.

.github/FUNDING.yml is deleted rather than repointed. It routed sponsorship to
a Polly maintainer, and neither Brighter nor Darker has one.

Refs P1.1-P1.7.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014HCSZBA6HSzikr1XTg7bPp
The plan's §2.3 claimed no public type or member name contains "Polly", and
called it "the single most important finding" - the fact D1 rests on. It is
false. `Polly.PollyServiceCollectionExtensions` is public, with 8 public
members.

The verification behind the claim had two independent bugs:

  git grep -ohE "\b[A-Za-z_]*Polly[A-Za-z_]*\b" -- "src/*.cs"

`\b` is a GNU extension this platform's POSIX ERE does not support, so the
pattern matches nothing at all - `git grep -cE "\bPolly\b"` on a file with six
hits exits 1. And `src/*.cs` matches only files directly in `src/`, of which
there are none. Two reasons to return empty, neither of them the absence of
matches. The corrected check runs against the P0.3 public-API baseline instead
of the source, and is in the plan.

The same false negative hid six embedded identifiers that Pass 1 must handle:
PollyVersion (54 hits), PollyConfig, EnablePollyMetering, ListenPollyMetrics,
and the two ServiceCollectionExtension names. Without them the dotted
replacement produces Paramore.FencesVersion and the like.

D12 records the resulting public API change: PollyServiceCollectionExtensions
becomes ResilienceServiceCollectionExtensions, dropping the brand token rather
than swapping it. The rest of the surface - ResiliencePipeline,
ResilienceContext, ResiliencePipelineBuilder - is already brand-free; Polly*
was the odd one out, and the fork is the moment to fix that rather than carry
it forward as Fences*. Consumers never name the class, so migration cost is
nil either way.

eng/rename-to-fences.py implements Phase 2a and 2b. Three things in it are
load-bearing: Pass 1 runs before the dotted replacement; both Pass 2 rules are
case-sensitive, which is what keeps resilience.polly.* from becoming
resilience.paramore.fences.* (D4); and http(s) URLs are masked throughout,
because ~34 comments cite App-vNext/Polly issues that explain the code they sit
beside, plus the Polly-Contrib jitter link.

Markdown is out of scope for the script, recorded as 2b-bis. Renaming prose
mechanically manufactures falsehoods rather than merely reading clumsily: it
misquotes third-party article titles in docs/community/resources.md, invents a
Fences-Contrib organisation, and produces "migrating from Fences v7" for a
version that never existed. Documentation is Phase 7 (D8 already owns the
pollydocs.org links) and is editorial work, not substitution. The corollary is
that P2.3's `git grep -in polly` will still return every *.md after the rename
commit - expected, not a miss.

Refs P2.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014HCSZBA6HSzikr1XTg7bPp
One scripted, reproducible pass: eng/rename-to-fences.py, run once from a clean
tree. Namespaces, assemblies, package IDs, project and folder names, and the
solution file all move from Polly.* to Paramore.Fences.*. Paths move with git mv
so rename detection survives; this commit's SHA goes into .git-blame-ignore-revs
so blame still reaches the real author of each line.

Validated three ways. The build is green - 0 errors, 0 warnings - with test
counts identical to the pre-rename baseline on every target framework: Specs
2115, Core 777, Extensions 111, RateLimiting 34 plus 1 skipped, Testing 7. All
five packages pack. Across the 1,725 lines of public API baseline the only diff
is the one deliberate type rename below. Every remaining "polly" outside
Markdown was triaged; the survivors are listed in the plan.

Three things in the script are load-bearing:

- Pass 1 rewrites identifiers with Polly glued inside them before the dotted
  replacement runs, or PollyStrongNamePublicKey becomes
  Paramore.FencesStrongNamePublicKey. The plan listed four such identifiers; it
  is actually ten - see the previous commit for why they were missed.
- Both Pass 2 rules are case-sensitive. That is what keeps Polly ->
  Paramore.Fences away from lowercase resilience.polly.*, so polly -> fences can
  turn it into resilience.fences.* (D4) rather than
  resilience.paramore.fences.*. It also means uppercase POLLY_ never matched -
  the POLLY_UPDATER_BOT_* secrets in six workflows survive for Phase 6.
- http(s) URLs are masked. Around twenty code comments cite App-vNext/Polly
  issues that explain the code they sit beside, and Polly.Contrib.WaitAndRetry
  is a third-party package; rewriting either would break real references.
  Polly.Contrib.* is masked for the same reason after the first run invented
  Paramore.Fences.Contrib.WaitAndRetry and failed restore.

Markdown is out of scope. Renaming prose mechanically manufactures falsehoods -
misquoted third-party article titles, a Fences-Contrib organisation that does
not exist, "migrating from Fences v7" for a version that never shipped.
Documentation is Phase 7 and is editorial work. `git grep -in polly` therefore
still returns every *.md; that is expected.

The one public API change (D12): PollyServiceCollectionExtensions becomes
ResilienceServiceCollectionExtensions, dropping the brand token rather than
swapping it, because the rest of the surface - ResiliencePipeline,
ResilienceContext, ResiliencePipelineBuilder - carries no brand. Consumers never
name the class, so migration is unaffected. Telemetry moves per D4: the meter
and ActivitySource become Paramore.Fences, metrics become resilience.fences.*.

Four things had to be pulled forward from Phase 3, because Phase 2 cannot be
green without them:

- Fences.snk is generated (2048-bit, D5's openssl recipe) and Polly.snk is
  deleted. Without a key file every signed assembly fails CS7027.
  FencesStrongNamePublicKey now holds our public key, not Polly's - otherwise
  InternalsVisibleTo would still be granting access to Polly-signed assemblies.
  test/Shared/StrongNameTests.cs hard-codes both the key and its token, so it
  changes here too, split across lines because 576 hex characters exceeds the
  500-character limit that Polly's 1024-bit key fit under.
- PackageValidationBaselineVersion pointed at 8.5.2, which restore tried to
  download as Paramore.Fences.Core 8.5.2. Commented out until 9.0.0 ships.
- The samples referenced published Polly packages and there is no published
  Paramore.Fences.* to switch them to. All eight - including the F# and VB ones,
  where the plan counted seven - now use ProjectReference into src/. This is the
  preferred shape, not a stopgap.

Microsoft.Extensions.Http.Resilience has a hard dependency on Polly, so its
AddResilienceHandler hands the callback a Polly builder that no Fences strategy
can attach to. Rather than delete that documentation, the snippets and the Chaos
sample take an explicit Polly.Core reference and use Polly's API deliberately,
with comments saying why. This is why Snippets.csproj loses its project-wide
`<Using Include="Paramore.Fences" />`: both libraries export
ResiliencePipelineBuilder, PredicateResult and DelayBackoffType, and a global
using cannot be opted out of per file. 23 snippet files gain an explicit import
instead, and Pattern_CircuitPerEndpoint moves to HttpClientIntegrations.cs -
transparent to the docs, because mdsnippets resolves regions by name.

That limitation bounds what the fork achieves and is recorded in ADR 0002:
Fences removes Brighter's Polly dependency, but it cannot remove Polly from an
application that calls AddStandardResilienceHandler.

Refs P2.1, P2.2, P2.3, and P3.1/P3.2 in part.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014HCSZBA6HSzikr1XTg7bPp
ad904b6 rewrote 885 files mechanically. Without this, blame on any renamed
line stops at the rename instead of reaching the person who wrote it.

Note that git does not read this file unless told to - contributors need
`git config blame.ignoreRevsFile .git-blame-ignore-revs` locally. GitHub's blame
UI honours it automatically. CONTRIBUTING.md should say so; that is Phase 4.

Verified: blame through RetryHelper.cs still resolves to martintmk's 2023
commit, across both the rename and the src/Polly.Core -> src/Paramore.Fences.Core
move.

Closes P2.4.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014HCSZBA6HSzikr1XTg7bPp
Phase 3 of the fork migration. Package metadata, descriptions and the
reader-facing documents now describe Fences rather than Polly.

eng/Library.targets: Company and Authors become Brighter Command, and
PackageProjectUrl and PackageReleaseNotes point at BrighterCommand/Fences.
Copyright retains App vNext's notice alongside ours rather than replacing
it — guardrail G2 constrains the authorship fields, but BSD-3-Clause
clause 1 requires the upstream notice on a derivative work, and LICENSE
already carries both lines.

All five package descriptions open with the lineage sentence immediately
followed by the non-affiliation line, per guardrails G1 and G3. PackageTags
drop Simmy and the dotted Paramore.Fences token left over from the rename.

MinVerMinimumMajorMinor moves to 9.0, so the packages now build as
9.0.0-alpha.*.

docs/migration-from-polly.md is new: the package-ID and namespace maps, the
one renamed public type, the telemetry names — which is the only part a
consumer cannot fix with a find-and-replace over their own source — the
strong-name change, and the bounded claim about
Microsoft.Extensions.Http.Resilience, which keeps Polly in the graph
regardless. This closes P7.2 early.

README.md loses its broken link syntax, its empty NuGet badges, the
BrigherCommand typo and its wrong package IDs, and gains the fork
provenance note and a migration section. package-readme.md was still
wholly Polly's; its NuGet version badge is removed rather than repointed
because nothing is published yet. docs/index.md loses the assertion that
Polly is part of the .NET Foundation, which was never true of Fences. The
docfx site is titled Fences, the project, not Paramore.Fences, the package.

Two decisions recorded in the plan. P3.2a: carry the PublicAPI.Shipped.txt
baseline forward rather than emptying it, which keeps the pre-rename oracle
comparable and means the D12 rename lives in Shipped.txt. P3.7: the
pollydocs.org links move to Phase 7, where the wordlist entry they depend
on is changed in the same commit — the plan had contradicted itself here.

Full build green: 3044 passed and 1 skipped on each of net8.0, net9.0 and
net10.0, matching the baseline, and the public API diff is still the one
type rename and nothing else.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014HCSZBA6HSzikr1XTg7bPp
Phase 4 of the fork migration. Every contributor-facing document was still
Polly's, and several of them bound contributors to the .NET Foundation or
routed them to Polly's Slack and issue tracker. None of that is ours to
offer.

Rewrite CONTRIBUTING.md on Brighter's section structure but with this
repository's actual conventions, read out of .editorconfig, eng/analyzers
and Stylecop.json rather than assumed from Brighter: PascalCase constants,
no licence header region, expression-bodied members enforced as errors,
test/ not tests/, Shouldly and NSubstitute and FsCheck, Stryker at 100%,
and the .PublicAPI discipline that has no Brighter equivalent. Public API
changes get their own section because it is what contributors and coding
agents get wrong most often.

Replace CODE_OF_CONDUCT.md with the Contributor Covenant, matching
Brighter, and say plainly that Fences is not a .NET Foundation project.

Rewrite SECURITY.md and .github/IRP.md around advisories on this
repository, with the four maintainers as fallback. The incident response
checklist gains a triage branch for a vulnerability inherited from Polly:
if the code predates the fork then Polly and everything downstream of it
is affected too, and disclosure has to be coordinated upstream.

Add CLA.txt and wire up CLA Assistant. Sections 1 to 6 of the agreement
are Brighter's verbatim; only the project name and the execution
instruction change, because clahub has been defunct for years. Signatures
are stored on a cla-signatures branch of this repository, so the workflow
needs no token beyond GITHUB_TOKEN.

Rework the issue forms and pull request template, and drop the legacy
.github/ISSUE_TEMPLATE.md that the forms directory superseded. The bug
form now asks whether the behaviour also occurs in Polly, which tells us
straight away whether a defect is ours or inherited.

Add CODEOWNERS.

Nothing under src/, test/, eng/ or cake.cs changed, so the build cannot
have regressed. markdownlint-cli2 reports no issues on any changed
Markdown file and every changed YAML file parses.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014HCSZBA6HSzikr1XTg7bPp
The rename script mechanically turned Polly's cname, www.pollydocs.org,
into www.fencesdocs.org — a domain nobody owns. Remove it. D8 already put
the site on GitHub Pages, and there is no point buying a domain while
Phase 8 is held pending the Polly negotiation: if those talks succeed the
fork may be retired, and brightercommand.github.io/Fences costs nothing
and works today.

The line had never taken effect. The site is already served from
https://brightercommand.github.io/Fences/ and the repository's Pages
configuration reports cname: null, so this only stops the next deployment
pointing somewhere that does not resolve.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014HCSZBA6HSzikr1XTg7bPp
The gh-pages workflow lints and spellchecks every Markdown file, but the
step had never been run against what Phases 1 to 3 added, and it was
failing on four tracked files.

Give NOTICE.md's licence fence a language, and space the separator row of
ADR 0002's decision table.

Both ADRs tripped MD025 by having a front matter title and a body
heading. That is not a defect in the documents: the front matter title is
page metadata for docfx and the body heading is the document's own, which
is what Nygard's format calls for. Configure MD025 to stop counting front
matter as an h1, so every ADR we write from here passes rather than just
these two.

Exclude fork-migration-plan.md from both gates. It is a working document
that will be retired when the migration completes, MD004 wanted 86 of its
bullets rewritten for style alone, and it carries around forty words the
dictionary does not know. Nobody editing a migration decision should be
thinking about bullet characters.

Add sixteen more words to the list for the tracked files that remain in
scope. Note that aspell is not available locally, so unlike the Markdown
lint the spelling side is reasoned from the pipeline's filters rather
than proven.

Verified with the tool the workflow pins, markdownlint-cli2 0.23.2, over
its exact glob set: no tracked file fails. PROMPT.md and baseline/README.md
still fail but are gitignored, so CI never checks them out.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014HCSZBA6HSzikr1XTg7bPp
Fences had Polly's AGENTS.md and an interim CLAUDE.md stub pointing at the
migration plan. Neither told an agent what this repository's conventions
actually are, and the conventions here are not Brighter's — several are its
exact opposite. An agent carrying Brighter context into this repo gets
constants, licence headers, test naming and InternalsVisibleTo wrong.

Add `.agent_instructions/`, inferred from `.editorconfig`, `eng/analyzers/`,
`eng/*.targets` and `cake.cs` rather than imported from Brighter, and agreeing
with CONTRIBUTING.md, which stays the source of truth. `public_api.md` has no
Brighter equivalent and exists because the `.PublicAPI/` baseline is what
agents get wrong most often here; the layout is flat, one pair of files per
project, contrary to what the migration plan assumed.

Rewrite AGENTS.md as an index into it, replace the CLAUDE.md stub, and add
`.github/copilot-instructions.md`.

Port `.claude/commands/` from Brighter — tdd, refactor, adr, spec and bugfix —
with the adaptations that would otherwise rot: `tests/` to `test/`, `master` to
`main`, `When_X_Then_Y` to `Member_Scenario_Outcome`, one-test-per-file to
grouping by type under test, `ALL_CAPS` constants and licence headers removed,
Docker test infrastructure dropped because there is none, and the messaging
examples rewritten in resilience terms. The verify steps gain this repo's real
gates: the `.PublicAPI/` baseline, analyser cleanliness, the Stryker score, and
re-running mdsnippets when `src/Snippets` changes.

`generate_adr_index.awk` comes with them, so `docs/adr/index.md` is now
generated. `specs/` and `bugfixes/` hold the state the workflows write.

`.claude/settings.json` allows the read-only and build commands this repo
actually uses; `dotnet nuget push` is explicitly denied while Phase 8 is held.

Markdown lint is clean over the workflow's exact glob set. The word list grows
by 68 entries; the spell check still cannot be run locally.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014HCSZBA6HSzikr1XTg7bPp
A security review of the Phase 5 commit flagged allowlist entries that grant
more than their names suggest.

Remove `Bash(sed:*)`, which nothing in this repository uses, and
`Bash(sort:*)`. Narrow `Bash(find:*)` — which permits `-exec` and `-delete` —
to the three read-only forms actually needed. Pin the `npx markdownlint-cli2`
allow to `markdownlint-cli2@0.23.2`, matching the version CI resolves, so an
unpinned npx cannot fetch whatever the registry currently serves;
`.agent_instructions/documentation.md` prints the pinned form for the same
reason.

Add denies for `git push --force`, `git reset --hard`, `git clean -fdx`,
`gh pr merge` and `gh release create`/`upload`. Deny beats allow, including a
command file's own `allowed-tools` frontmatter — and the ported commands
declare `Bash(git:*)`, `Bash(gh:*)`, `Bash(dotnet:*)` and, in two cases, bare
`Bash`. The frontmatter is left as ported so the files stay close to
Brighter's; the deny list is what constrains them.

Kept knowingly, and recorded in PROMPT.md: `Bash(awk:*)`, because the ADR
index generator needs it; the dotnet and Cake build commands, because MSBuild
runs `Exec` tasks and Cake scripts are arbitrary C#, so there is no way to
allow building this repository without it; and `cat`/`echo`, whose redirection
permits writes.

`Bash(dotnet nuget push:*)` was already denied and stays denied, so the
Phase 8 publication hold survives the `Bash(dotnet:*)` the spec and bugfix
commands grant themselves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014HCSZBA6HSzikr1XTg7bPp
Fences carries Polly's history but has no upstream remote, so drift is
invisible unless someone checks for it. Upstream main was 5 commits ahead of
our fork point 47e3b41: all Dependabot, no source changes, no bug fixes, no
public API change.

Every one of our pre-bump values matched upstream's exactly, so this is a
clean apply rather than a merge — 8 versions in Directory.Packages.props and
3 github/codeql-action pins (v4.37.6 to v4.37.7) across code-ql.yml and
ossf-scorecard.yml.

Polly.Core stays at 8.7.0, which is still upstream's latest release. The
direct reference from src/Snippets and samples/Chaos is deliberate and is not
what this commit is about.

Verified with a full Cake Default run: 3044 passed / 1 skipped on each of
net8.0, net9.0 and net10.0, matching the pre-rename baseline; solution build
clean; AOT validation green; public API diff still the D12 rename and nothing
else. The xunit.runner.visualstudio 3.1.5 to 4.0.0 major bump changed no test
outcome.

One new warning comes with Microsoft.NET.Test.Sdk 18.9.0: compiling cake.cs
emits CS7022, because the script declares ProjectType=Test and 18.9.0 injects
an entry point that collides with its top-level statements. It is cosmetic and
does not fail the build, but it is new and it is recorded in PROMPT.md.

Fences inherited Polly's dependabot.yml with the same groupings, so once this
branch reaches main these bumps arrive on their own. This hand-sync was only
needed because the branch is unmerged and Dependabot has raised nothing yet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014HCSZBA6HSzikr1XTg7bPp
Polly's workflows were good, and most of them survive. What could not survive
was everything wired to infrastructure Fences does not have: an Azure Key Vault
code-signing certificate, a Codecov account, and two GitHub Apps in the
App-vNext organisation. Those steps were not merely misnamed — they could not
run at all.

Delete three workflows outright. updater-approve.yml gates on a bot account
that will never exist; nuget-packages-published.yml is driven by the
repository_dispatch that goes with the app token; update-dotnet-sdk.yml calls a
reusable workflow out of a Polly maintainer's personal repository, which is an
external dependency and an app token we would otherwise have to provision.
Dependabot still covers NuGet packages and global.json can be bumped by hand.

Drop Authenticode signing, per D6, matching Brighter and Darker. The sign job
goes, and with it eng/signing/filelist.txt and the authenticodelint and sign
tool entries. validate-signed-packages becomes attest-packages: with signing
gone it duplicated validate-packages' dotnet validate run and its signature
step had nothing to inspect, so what survives is the SBOM download, the extract
and actions/attest, still gated on a tag.

Drop Codecov, per P6.3, rather than provision another secret on day one.
Coverage is still collected by coverlet and published as the coverage-<os>
artefact and a job step summary. The badge goes from package-readme.md, the
claim from CONTRIBUTING.md, and codecov/codecov-action from the Dependabot
auto-approve list.

Resolve the POLLY_UPDATER_BOT and POLLY_REVIEWER_BOT secrets by falling back to
GITHUB_TOKEN in the three workflows that outlive them, rather than creating two
GitHub Apps — the same call already made for cla.yml. Two consequences are
worth knowing. Pull requests opened by after-release.yml and on-push-do-docs.yml
will not trigger build.yml, because GitHub suppresses workflow runs for events
raised with GITHUB_TOKEN. And GITHUB_TOKEN cannot hold workflows: write, which
the app token did, so dependabot-approve.yml cannot auto-merge a pull request
touching .github/workflows; the github-actions group needs merging by hand.

Delete eng/bump-version.ps1 rather than repair it. It was broken twice over:
System.Version cannot parse a SemVer pre-release such as 9.0.0-alpha001, and
there are no Paramore.Fences* PackageVersion entries in Directory.Packages.props
for it to update, because our samples use ProjectReference where Polly's took
package references. It had nothing to do.

Arm publish-nuget rather than gate it, per D13, which reverses the Phase 8 hold.
Holding traded away two things it should not have: the package identifiers,
which anyone can take while we wait, and user feedback, which cannot come from a
repository nobody can consume. Both are bought back by publishing pre-releases,
which make no compatibility promise. Versions are 9.0.0-alpha001 onwards —
zero-padded and undotted, because 9.0.0-alpha.001 is invalid SemVer and the
padding is what makes alpha010 sort after alpha009. None of this is configured
in MSBuild; MinVer takes the tag verbatim.

README.md and package-readme.md now both say that Fences may be retired if App
vNext reverses the Open Source Maintainers Fee, and that migrating back is the
same change in reverse. D13 rests on that wording. Publishing by hand remains
denied in .claude/settings.json: CI publishes, from a tag, and nothing else.

The fork guards and the SDK pin needed verification, not edits. NUGET_USER plus
a nuget.org trusted-publisher policy is now the only secret the repository still
needs; STRYKER_DASHBOARD_API_KEY is inert, because dashboard is not among the
configured Stryker reporters.

No file under src/, test/ or eng/analyzers/ changed, so the suite was not re-run.
Verified with dotnet tool restore, dotnet mdsnippets --validate-content (no
rewrites), a YAML parse of all thirteen remaining workflows, and markdownlint
over CI's exact glob set, which is clean on every tracked file. actionlint and
zizmor still cannot run in this environment.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014HCSZBA6HSzikr1XTg7bPp
The rename script deliberately skipped every Markdown file, because renaming
prose mechanically manufactures falsehoods rather than merely reading clumsily.
That left the documentation describing Polly while the code described Fences.
This is the editorial pass that reconciles them.

Four things were broken rather than merely stale, and none were in the plan.

Thirty-one `xref:Polly.*` cross-references pointed at a namespace that no
longer exists; they are now `xref:Paramore.Fences.*` and resolve against the
generated API metadata. `docs/community/toc.yml` referenced `fences-contrib.md`,
a file that never existed — Phase 2 renamed the structural toc but not the prose
page it pointed at. The telemetry documentation was factually wrong, describing
a `Polly` meter and logger and `resilience.polly.*` instruments where the code
emits `Paramore.Fences` and `resilience.fences.*`. And the illustrative stack
traces showed `at Polly.ResiliencePipeline` in `/_/src/Polly.Core/`.

Every `pollydocs.org` link becomes the equivalent path under
`https://brightercommand.github.io/Fences/`, verified live: the extensionless
deep-link form resolves and a bogus path returns a real 404. The ones visible in
`docs/**` and `README.md` all sat inside generated snippet regions, so the fix
is in `src/Snippets/Docs/*.cs` with the Markdown regenerated. `fencesdocs`
leaves the spellcheck wordlist in the same commit, as P7.3 required.

Prune `docs/community/`. `polly-contrib.md` describes Polly's GitHub
organisation and Slack, which Fences has no equivalent of. `resources.md` was
thirty-three third-party article and podcast titles that cannot be rewritten
without misquoting their authors. `git-workflow.md`, `http-client-integrations.md`
and `libraries-and-contributions.md` are kept and rewritten; the last of these
now says whose acknowledgements it carries.

Delete the Polly- and Simmy-branded binaries: ten cheat-sheet PDFs, their ten
PowerPoint sources, and the Simmy logo. They cannot be regenerated here, and
shipping another project's branding from the Fences site is the most visible
inconsistency available to us. Recreating the cheat sheets is separate work.

Some documentation stays Polly-facing on purpose. `http-client-integrations.md`
and `samples/Chaos/README.md` now lead with the bounded claim: because
`Microsoft.Extensions.Http.Resilience` hands its callback a Polly
`ResiliencePipelineBuilder`, their samples use Polly's API and cannot do
otherwise. `migration-v8.md` and `v7/extensibility.md` are Polly's v7-to-v8
material, reframed as API generations rather than product versions — Fences
ships the pre-v8 API as `Paramore.Fences`, so the guide is genuinely useful, but
there was never a Fences v7 and both pages say so. Upstream citations survive
untouched.

Fix `generate_adr_index.awk`, which was emitting two CI failures: a relative
link escaping the docfx root, and a table delimiter that MD060 rejects. A
previous session had hand-fixed the generated file, which that file forbids, so
regenerating reintroduced both.

The bare-substitution trap is real and worth recording. Even behind a sentinel
list protecting the upstream citations, the automated pass produced "Fences is a
community fork of [Fences]", claimed Polly-Samples were "written for Fences",
and corrupted a Polly wiki URL into a page that does not exist. All were caught
by reading the diff.

Verified: `docfx metadata` and `docfx build` both exit 0 with 0 warnings from a
clean `_site` — the first successful site build since the rename — with the
resolved xrefs spot-checked in the generated HTML. `dotnet mdsnippets`
regenerated with no drift, `dotnet build src/Snippets` is 0 warnings / 0 errors,
and markdownlint over CI's exact glob set is clean on every tracked file. No
behavioural code changed; the only files under `src/` touched are READMEs and
comments inside snippet regions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014HCSZBA6HSzikr1XTg7bPp
Trusted publishing needs an owner chosen before the first push, and the choice
is effectively irreversible: a policy applies to all packages owned by the
selected owner, and all five Paramore.Fences IDs are new, so the first push
creates them owned by whoever the short-lived key is scoped to. Getting this
wrong means nuget.org's manual co-owner invite flow to correct it.

Record D14: the packages and the policy are owned by the BrighterCommand
account, not a personal one. P8.1 is the strongest reason — a prefix
reservation is granted to an owner, and the application's own argument is that
120 of the 121 Paramore.* packages belong to BrighterCommand. Publishing Fences
personally would exempt it from the reservation being applied for. Beyond that:
all 123 existing Paramore.* packages are owned by BrighterCommand, verified
against the nuget.org search API; and D11 names four maintainers, only one of
whom could administer personally-owned packages.

Expand P8.3 from a one-line reminder into the full walkthrough, since it is now
the only thing between the branch and a first publish: the five policy fields
with our values, the two GitHub-side items that do not exist yet, the 1-hour
key lifetime and single-use token, and the 7-day pending-activation window that
should not apply to us because the repository is public and not a fork.

Two uncertainties are recorded rather than papered over. NUGET_USER is set to
BrighterCommand on the reasoning that nuget.org organizations and users share
one account namespace, but neither Microsoft Learn nor the NuGet/login README
states what an org-owned policy expects — so the note says to try the personal
username if the exchange returns 403. And whether BrighterCommand is formally
an organization or a shared user account could not be determined from its
profile page; the Trusted Publishing page settles it on sight.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014HCSZBA6HSzikr1XTg7bPp
Fences-Policy was created on nuget.org on 2026-08-24. Every field matches the
workflow: package owner BrighterCommand per D14, glob Paramore.Fences* (which
covers all five IDs including the bare Paramore.Fences), workflow build.yml,
environment NuGet.org matching publish-nuget's environment key exactly. The
numeric GitHub IDs it captured, Repository Owner #24714628 and Repository
#1338118962, match the GitHub API.

Because it captured those IDs on creation the policy is permanently active, not
temporarily active for seven days, so the pending-activation caveat P8.3 warned
about never applied. The open question about whether BrighterCommand is an
organization is also closed: it was selectable as package owner, and GitHub
reports the owner type as Organization.

Record two things learned since. Guessing NUGET_USER wrong is cheap, because
NuGet/login runs before dotnet nuget push, so a bad value fails the job with
nothing published and the failed job can be re-run once the secret is fixed.
And because we publish from a tag, the NuGet.org environment's deployment
branches and tags rule must stay permissive or carry a matching tag pattern,
or the job will never start.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014HCSZBA6HSzikr1XTg7bPp
@github-advanced-security

Copy link
Copy Markdown

You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool.

What Enabling Code Scanning Means:

  • The 'Security' tab will display more code scanning analysis results (e.g., for the default branch).
  • Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results.
  • You will be able to see the analysis results for the pull request's branch on this overview once the scans have completed and the checks have passed.

For more information about GitHub Code Scanning, check out the documentation.

Comment thread .github/workflows/cla.yml
steps:

- name: Check the contributor has signed the CLA
uses: contributor-assistant/github-action@ca4a40a7d1004f18d9960b404b97e5f30a505a08 # v2.6.1
iancooper and others added 3 commits August 25, 2026 09:54
Three failures on PR #2, the first time any of this work has run through CI.

**Windows build.** `__ValidateDocs` failed with a missing snippet:
`.agent_instructions/documentation.md` illustrates what a MarkdownSnippets
region looks like, and does so with a literal `<!-- snippet: ... -->` marker at
the start of a line inside a fenced block. MarkdownSnippets matches the marker
regardless of the fence and cannot find a matching region, because none exists —
the name is an example.

It passed on macOS and Ubuntu because MarkdownSnippets does not descend into
dot-prefixed directories there, they being hidden, but does on Windows. That
silent platform divergence is the real defect, so the fix is to exclude
`.agent_instructions` and `.claude` in `mdsnippets.json`: they are agent
guidance, never published documentation, and MarkdownSnippets has no business
rewriting them on any platform. The equivalent markers in `CONTRIBUTING.md` are
inline in backticks and were never matched.

**Spellcheck.** Sixteen words, from the one CI gate that cannot be run locally —
`aspell` is not installable here, so `.github/wordlist.txt` had been grown on
reasoning alone across four sessions. Fifteen are added to the wordlist. Most
predate this branch's documentation work and are simply prose that had never
been checked: `sexualized` is Contributor Covenant text and must not be edited,
and `Brighter's`, `Nygard's` and `vNext's` are possessives of proper nouns.

The sixteenth, `Fences's`, is not a dictionary gap but an artefact of the Phase 7
rename pass turning "Polly's" into it. A name ending in s does not take that
possessive. The two occurrences in spellchecked documentation are reworded to
avoid the possessive entirely, and the remaining nine become `Fences'`.

Also learned, and worth recording: the spellcheck does **not** cover
dot-directories. Zero files under `.agent_instructions` or `.claude` were
processed, so the claim that both are inside both CI gates was wrong.

`dependency-review` also failed, but on a repository setting rather than
anything in the tree — Dependency graph is not enabled — so it is untouched here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014HCSZBA6HSzikr1XTg7bPp
The remaining CI failure was the spell check, which every previous session
recorded as impossible to run locally because `aspell` was not installed. That
was wrong: `brew` is available. Installing it, plus `pyspelling` and
`pymdown-extensions` in a virtualenv, runs CI's exact configuration in seconds.

Doing so turns an iterative push-and-see loop into a local check, and it found a
word CI had not reported yet. Six entries are added to the wordlist, all of them
legitimate prose in the two ADRs, which were written in Phase 1 and had never
been spell-checked: migrators, Pryce's, reviewable, unprefixed, unquantified and
unresolvable. Tracked-file failures are now zero locally.

Record the recipe in `.agent_instructions/documentation.md`, with the two things
that make it usable. `pymdown-extensions` is required, because `spellcheck.yml`
asks for `pymdownx.superfences` and `pyspelling` dies without it. And failures
must be filtered to files `git ls-files` lists: `pyspelling` walks the working
tree, so it reads gitignored files that CI never checks out, which is the
difference between 51 reported words and the 6 that mattered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014HCSZBA6HSzikr1XTg7bPp
Running the spell check locally makes `pyspelling` compile
`.github/wordlist.txt` into a binary `dictionary.dic` in the repository root.
It is build output, not source, and the previous commit picked it up. Remove it
and ignore it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014HCSZBA6HSzikr1XTg7bPp
@iancooper
iancooper merged commit e972cc5 into main Aug 25, 2026
22 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Aug 25, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants