Skip to content

Add rate-limit spec validation rules RL003-RL008 and Node 24 CI bumps - #62

Merged
davidkallesen merged 6 commits into
mainfrom
feature/ATC_API_RL003
Jul 28, 2026
Merged

Add rate-limit spec validation rules RL003-RL008 and Node 24 CI bumps#62
davidkallesen merged 6 commits into
mainfrom
feature/ATC_API_RL003

Conversation

@davidkallesen

Copy link
Copy Markdown
Contributor

Summary

  • Catch x-ratelimit-* misconfiguration at build time
  • Surface config that parses but breaks or is ignored
  • Prevent a startup crash and a generated-code compile error
  • Move all workflow actions onto Node 24 runtimes
  • Drop two unmaintained third-party actions

Changes

✨ Features

  • Add ATC_API_RL003: one policy name with conflicting settings
  • Add ATC_API_RL004: policy names colliding on one constant
  • Add ATC_API_RL005: values the limiter rejects at startup
  • Add ATC_API_RL006: x-ratelimit-enabled outside an operation
  • Add ATC_API_RL007: algorithm cannot supply Retry-After
  • Add ATC_API_RL008: window-seconds on a concurrency policy
  • Expose ParseAlgorithm so the validator can reuse it

👷 CI/CD

  • Bump actions/checkout v4 to v7
  • Bump actions/setup-dotnet v4 to v6
  • Pin dotnet/nbgv from master to v0.5.2
  • Replace nelonoel/branch-name with github.ref_name
  • Replace satak/webrequest-action with a curl step
  • Fail the job when cache invalidation fails

📝 Documentation

  • List RL and STREAM in the rule category summary

Notes

  • RL004 is an Error: colliding names emit duplicate constants
    and generated code fails with CS0102
  • Matches the existing ATC_API_SCH018 precedent for schemas
  • RL005 values throw from the limiter constructor inside
    AddApiRateLimiting, so the app never starts
  • RL007 is Info because the flag defaults on, making it a
    trade-off to surface rather than an authoring error
  • Only fixed and token-bucket carry a Retry-After value;
    sliding advertises the name but never a value
  • RL007 is evaluated against the first-wins policy set the
    generator emits, so it names the real algorithm
  • RL003-RL006 and RL008 compare only explicitly declared values
  • That avoids false positives on inherit-without-repeating specs
  • No rule fires on any existing scenario or sample spec
  • RL007 emits Info only, so sample builds stay clean
  • No @v4 or @master action references remain
  • Watch the first release run: checkout v6 changed credential
    persistence, and two jobs push with a PAT
  • There is no dependabot.yml, so nothing bumps these actions

…ettings (RL003)

A policy name is the unit of limiter registration in RateLimiterOptions, so one
name maps to exactly one limiter and cannot hold two permit limits or two
partition strategies. CollectPolicies is first-wins, so declaring the same name
at several sites with different settings silently discards all but the first.

Reported by a consumer who had two endpoints sharing 'logs-read' and wanted
different partitions for each. They discovered the constraint by reading the
wiki; the build should have told them.

RL003 compares only settings EXPLICITLY declared at each site, not the fully
resolved values. That distinction matters: re-declaring a policy name on a
sub-path without repeating every setting is the idiomatic style across this
repo's specs and samples ("# Inherit accounts policy"), and it contradicts
nothing. Comparing resolved values instead would flag every such site, because
the sub-path inherits document-level defaults the first site overrode - a false
positive on normal authoring. Verified: zero RL003 across all scenarios and
samples in a Release build.

Values parsed case-insensitively (partition, algorithm) are normalized before
comparison so 'user' and 'User' do not read as a conflict. Claim names are
compared verbatim, since claims are case-sensitive.

The message names the policy, the conflicting extension key, each competing
value and its site, and points at both fixes: identical values everywhere, or
split into separate policy names.

Covers permit-limit, window-seconds, queue-limit, algorithm, partition,
partition-claim and emit-retry-after, so it also catches the pre-existing
conflict classes this feature did not introduce.
GitHub is deprecating Node 20 on Actions runners, and node20 actions are already
being force-run on Node 24. Audited every `uses:` across the three workflows
rather than only the two named in the warning.

Bumps (verified node24 via each action.yml `runs.using`, and every input we pass
still exists in the new major):
- actions/checkout        v4 -> v7
- actions/setup-dotnet    v4 -> v6
- actions/setup-node      already v6 (node24), unchanged

Also removes the two remaining non-node24 actions, both of which were pinned to
a moving `master` ref:
- nelonoel/branch-name@v1.0.1 was still **node12**, far past deprecation, and
  existed only to set $BRANCH_NAME. Replaced with a workflow-level
  `BRANCH_NAME: ${{ github.ref_name }}`. Equivalent here: the action derives the
  name from GITHUB_REF for push/workflow_dispatch and only diverges on
  pull_request events, which neither of these workflows uses.
- satak/webrequest-action@master was node20. It issued one unauthenticated GET,
  so it is now a curl step with --fail and bounded retries/timeout, which also
  makes a failed cache invalidation actually fail the job instead of passing
  silently.
- dotnet/nbgv is already node24 but was tracking `master`; pinned to v0.5.2.

No `@v4` or `@master` references remain. All three files re-validated as YAML.

Note: checkout v6 changed credential persistence to a separate file. The
merge-to-stable and release jobs push with a PAT via persisted credentials, so
that path is worth watching on the first run - it cannot be verified locally.
…mary

The class summary enumerated the rule categories but was never updated when the
STREAM and RL categories were introduced.
Brings the Node 24 action bumps onto the RL003 branch so both ship together.
Five diagnostics for x-ratelimit-* configuration that parses cleanly but breaks
the build, crashes the app at startup, or is silently ignored at runtime. Each
premise was verified against the runtime or the emitter rather than assumed.

RL004 (Error) - policy names colliding on the generated constant.
ToConstantName splits on '-', '_', ':' and space then PascalCases, so
'logs-read', 'logs_read', 'logs read' and 'LogsRead' all become 'LogsRead'.
RateLimitPoliciesExtractor emits one const per policy name with no dedupe, so a
collision emits duplicate members and generated code fails with CS0102. Error
matches ATC_API_SCH018, which does the same for schema names.

RL005 (Warning) - values the limiter constructor rejects. Verified all seven
variants throw ArgumentException: permit-limit <= 0, window-seconds <= 0 (fixed/
sliding/token-bucket), queue-limit < 0. These run inside AddApiRateLimiting
during DI setup, so the result is not a degraded limit but an app that fails to
start. permit-limit: 0 is the common case, so the message points at
x-ratelimit-enabled: false instead.

RL006 (Warning) - x-ratelimit-enabled outside an operation. It is the one
x-ratelimit-* extension that is NOT inherited: ExtractRateLimitConfiguration
reads it from the operation only, so declaring it on a path or the document
looks like it exempts everything beneath it but changes nothing.

RL007 (Info) - algorithm cannot supply a Retry-After value. Verified against
System.Threading.RateLimiting: a rejected sliding-window lease advertises the
RETRY_AFTER metadata name but TryGetMetadata returns false, and concurrency
never lists it; only fixed and token-bucket carry a value. So those two policies
return 429 with no header even though the flag defaults to true. Info rather
than Warning because the flag defaults on, making this a trade-off to surface
rather than an authoring error.

RL008 (Info) - window-seconds on a concurrency policy. ConcurrencyLimiterOptions
has no time component and the emitter drops the value. Takes precedence over
RL005 for a non-positive window, since a value that is never read cannot throw.

RL007 walks CollectPolicies rather than resolving per operation. An earlier
draft resolved per operation and mislabelled Showcase's 'users' as sliding (it
is fixed) and 'file-uploads' as sliding (it is token-bucket), because the
reported algorithm came from whichever operation was visited first rather than
from the first-wins config the generator actually emits. Cross-checked against
the generated Showcase output: the four reported policies are exactly the four
sliding/concurrency limiter registrations, and the fix also caught a missed
MultipartDemo 'global'.

RL004-RL006 and RL008 check each declaration site rather than resolved values,
matching RL003: what the author wrote at a site is always actionable, and
comparing resolved values produces false positives on the idiomatic
inherit-without-repeating style.
Missed by the earlier Node 24 sweep, which only audited .github/workflows/.
This file lives in docs/ and is a template users copy into their own repository,
so stale action versions propagate to every consumer rather than just to this
repo's CI.

- actions/checkout               v4 -> v7
- actions/setup-dotnet           v4 -> v6
- peter-evans/create-pull-request v6 -> v8

create-pull-request needed two majors: v7 is still node20, only v8 is node24.
Checked both majors for breaking changes against this template - v7 renamed
git-token to branch-token and dropped the deprecated PULL_REQUEST_NUMBER output
env var, neither of which this template uses, and all seven inputs it does pass
(token, commit-message, title, body, branch, delete-branch, labels) still exist
in v8. checkout v7's fork-PR restriction applies to pull_request_target and
workflow_run; this template triggers on push.

No node20-or-older action references remain anywhere in the repository.
@davidkallesen
davidkallesen merged commit 1362fa2 into main Jul 28, 2026
7 checks passed
@davidkallesen
davidkallesen deleted the feature/ATC_API_RL003 branch July 28, 2026 21:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant