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
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
**/bin/
**/obj/
**/TestResults/
**/node_modules/
**/.git/
**/.idea/
**/.vs/
Expand Down
45 changes: 43 additions & 2 deletions .githooks/pre-push
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,22 @@
# pre-push: run the ReSharper formatter + member reordering (ReformatAndReorder
# profile in RustPlusApi.sln.DotSettings) on the files being pushed and block the
# push when it produces changes, so unformatted/unordered code never leaves the
# machine. Bypass for emergencies with `git push --no-verify`.
# machine. When the credentials web app's browser script (or its lint setup) is
# among the pushed files, also run its ESLint mirror of SonarQube's JavaScript
# rules (apps/RustPlusApi.CredentialsWeb/eslint.config.mjs) and block the push
# on any finding. Bypass for emergencies with `git push --no-verify`.
set -u

repo_root=$(git rev-parse --show-toplevel) || exit 1
cd "$repo_root" || exit 1

default_branch=origin/develop
web_app=apps/RustPlusApi.CredentialsWeb

# Collect the files touched by the commits being pushed (stdin lines:
# "<local ref> <local sha> <remote ref> <remote sha>").
changed=""
web_changed=""
while read -r _local_ref local_sha _remote_ref remote_sha; do
# Deleting a remote branch pushes the zero sha — nothing to format.
case "$local_sha" in
Expand All @@ -33,8 +38,37 @@ while read -r _local_ref local_sha _remote_ref remote_sha; do
files=$(git diff --name-only --diff-filter=ACMR "$base" "$local_sha" -- \
'*.cs' '*.csproj' '*.props' '*.targets')
changed=$(printf '%s\n%s' "$changed" "$files")

files=$(git diff --name-only --diff-filter=ACMR "$base" "$local_sha" -- \
"$web_app/wwwroot/*.js" "$web_app/eslint.config.mjs" \
"$web_app/tsconfig.json" "$web_app/package.json" "$web_app/package-lock.json")
web_changed=$(printf '%s\n%s' "$web_changed" "$files")
done

web_changed=$(printf '%s' "$web_changed" | sort -u | sed '/^$/d')
if [ -n "$web_changed" ]; then
echo "pre-push: linting the credentials web app's browser script..."
if ! command -v npm >/dev/null 2>&1; then
echo "pre-push: npm is not installed, and the push touches $web_app's JavaScript."
echo "Install Node.js (CI runs the same lint and will reject the push's findings otherwise)."
echo "(Bypass once with: git push --no-verify)"
exit 1
fi
# The lockfile is the source of truth: install only when node_modules is missing, since
# `npm ci` would otherwise wipe and refetch it on every push.
if [ ! -d "$web_app/node_modules" ]; then
npm --prefix "$web_app" ci --no-fund --no-audit || exit 1
fi
npm --prefix "$web_app" run --silent lint || {
echo ""
echo "pre-push: ESLint found problems in $web_app (the same rules SonarQube applies)."
echo "Fix them, amend/commit, then push again."
echo "(Bypass once with: git push --no-verify)"
exit 1
}
echo "pre-push: lint OK."
fi

changed=$(printf '%s' "$changed" | sort -u | sed '/^$/d')
[ -z "$changed" ] && exit 0

Expand Down Expand Up @@ -66,7 +100,14 @@ dotnet jb cleanupcode RustPlusApi.sln \
--profile="ReformatAndReorder" \
--include="$include" \
--no-build \
--verbosity=ERROR || exit 1
--verbosity=ERROR
rc=$?
# Exit code 3 is ReSharper's "No items were found to cleanup": the push touched
# only .csproj/.props/.targets files, which the cleanup profile has no items
# for. There is nothing to compare, so that is a pass, not a rejection.
if [ "$rc" -ne 0 ] && [ "$rc" -ne 3 ]; then
exit 1
fi

after=$(hash_diffs)

Expand Down
12 changes: 12 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,15 @@ updates:
github-actions:
patterns:
- "*"

- package-ecosystem: "npm"
# The lint toolchain for apps/RustPlusApi.CredentialsWeb/wwwroot/app.js (ESLint + typescript-eslint
# + eslint-plugin-sonarjs), the local mirror of SonarQube's JavaScript rules. Dev-only: nothing
# from it ships in the app.
directory: "/apps/RustPlusApi.CredentialsWeb"
schedule:
interval: "weekly"
groups:
npm:
patterns:
- "*"
26 changes: 26 additions & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,29 @@ jobs:
# live-network adapter — are [ExcludeFromCodeCoverage] with justifications.
working-directory: ${{ github.workspace }}
run: python3 tools/coverage/check_threshold.py 95 90 TestResults/merged-web/Cobertura.xml

lint-web:
# The browser script in apps/RustPlusApi.CredentialsWeb/wwwroot has no .NET analyzer behind it, and
# Sonar.yml only analyzes develop after the merge. This runs the same JavaScript rules (the app's
# eslint.config.mjs mirrors SonarQube's) on every push and PR, so a smell fails here first.
runs-on: ubuntu-latest
defaults:
run:
working-directory: apps/RustPlusApi.CredentialsWeb

steps:
- name: Checkout Repository
uses: actions/checkout@v7

- name: Setup Node
uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
cache-dependency-path: apps/RustPlusApi.CredentialsWeb/package-lock.json

- name: Install lint toolchain
run: npm ci

- name: Lint wwwroot/app.js
run: npm run lint
6 changes: 5 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,15 @@ tools/coverage/report.sh # full suite + merged coverage report + C

dotnet tool restore
dotnet jb cleanupcode RustPlusApi.sln --profile="ReformatAndReorder"

npm --prefix apps/RustPlusApi.CredentialsWeb ci # lint toolchain for the web app's wwwroot/app.js
npm --prefix apps/RustPlusApi.CredentialsWeb run lint # ESLint mirror of SonarQube's JavaScript rules
```

## Critical Conventions

- **Never bump versions in project files.** `Version=1.0.0` is the local placeholder; CD injects real versions via `-p:Version=<tag>`.
- **Format before pushing.** The pre-push hook (`.githooks/pre-push`, wired by `dotnet build`) runs the ReSharper formatter and **rejects** the push on any diff. Run `dotnet jb cleanupcode` first.
- **Format before pushing.** The pre-push hook (`.githooks/pre-push`, wired by `dotnet build`) runs the ReSharper formatter and **rejects** the push on any diff. Run `dotnet jb cleanupcode` first. When the push touches `apps/RustPlusApi.CredentialsWeb/wwwroot/*.js`, the hook also runs that app's `npm run lint` (SonarQube's JavaScript rules) and rejects the push on any finding.
- **100% line/branch coverage is expected** for all new code. Exclusions require `[ExcludeFromCodeCoverage]` *with a justification comment*, and the member must be added to the exclusion list in [docs/development/testing.md](docs/development/testing.md).
- **`#if NET10_0_OR_GREATER` forks must be pinned by tests** asserting identical results on both TFMs. See the `HtmlColorParser` pattern in [docs/development/testing.md](docs/development/testing.md).
- **Stryker cannot mutate `RustPlusApi.csproj`** (protobuf-net.BuildTools breaks Stryker's rollback compiler). Pin core behavior with exact-assertion unit tests instead.
Expand Down Expand Up @@ -52,3 +55,4 @@ See [docs/development/testing.md](docs/development/testing.md) for the full guid
- C# `latest`, `Nullable enable`, `ImplicitUsings enable`
- 4-space indent, 120-column max, LF line endings (enforced by `.editorconfig`)
- All analyzer warnings are errors — fix them, don't suppress
- The web app's browser JavaScript (`apps/RustPlusApi.CredentialsWeb/wwwroot/app.js`) is linted by ESLint (`eslint.config.mjs` beside it, mirroring SonarQube's JS rules); an intentionally unawaited Promise is marked with `void` and a comment saying where its failures are handled
8 changes: 7 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ dotnet test RustPlusApi.sln -f net10.0
# and the net10.0-only web app, merged and checked separately so the app can't move the library bar.
tools/coverage/report.sh

# Lint the credentials web app's browser JavaScript (ESLint mirror of SonarQube's JS rules; also run by
# CI's lint-web job and by the pre-push hook when wwwroot/*.js or the lint setup changes):
npm --prefix apps/RustPlusApi.CredentialsWeb ci
npm --prefix apps/RustPlusApi.CredentialsWeb run lint

# Formatting + member reordering (ReSharper CLI; whitespace rules from .editorconfig, the
# ReformatAndReorder cleanup profile + member layout live in RustPlusApi.sln.DotSettings):
dotnet tool restore
Expand All @@ -42,7 +47,8 @@ docfx docs/docfx.json --serve

A committed **pre-push hook** (`.githooks/pre-push`, wired up automatically by the first
`dotnet build`) runs the ReSharper formatter **and member reordering** on outgoing files and
**rejects the push** if it changes anything — format before pushing.
**rejects the push** if it changes anything — format before pushing. It also runs the ESLint check
above when the push touches the web app's JavaScript or its lint setup.

## Architecture

Expand Down
10 changes: 10 additions & 0 deletions apps/RustPlusApi.CredentialsWeb/RustPlusApi.CredentialsWeb.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
<!-- The app is net10.0-only and deliberately outside the netstandard2.0 parity story
that governs src/. See docs/superpowers/specs/2026-09-03-credential-acquisition-website-design.md -->
<UserSecretsId>rustplusapi-credentialsweb</UserSecretsId>
<!-- tsconfig.json exists only so ESLint can type-check wwwroot/app.js (see eslint.config.mjs); it
must never make Visual Studio's TypeScript build step part of this project. -->
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
</PropertyGroup>

<ItemGroup>
Expand All @@ -16,4 +19,11 @@
<InternalsVisibleTo Include="RustPlusApi.CredentialsWeb.UnitTests" />
</ItemGroup>

<ItemGroup>
<!-- The lint toolchain's files, not app content: the Web SDK globs every *.json beside the project
as Content and would otherwise publish them (and ship them in the Docker image). -->
<Content Remove="package.json;package-lock.json;tsconfig.json" />
Comment on lines +23 to +25
<None Include="package.json;package-lock.json;tsconfig.json" />
</ItemGroup>

</Project>
34 changes: 34 additions & 0 deletions apps/RustPlusApi.CredentialsWeb/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Local mirror of the JavaScript rules SonarQube runs against develop (Sonar.yml), so a smell fails
// `npm run lint` here and in CI.yml instead of surfacing after the push. SonarQube's own
// `javascript:S9383` (Promises should not be left unhandled) and `S6544` (Promises should not be
// misused) are wrappers over typescript-eslint's type-aware rules, which is why a TypeScript program
// (tsconfig.json) is needed to lint plain JavaScript. The remaining Sonar way rules come from
// SonarSource's eslint-plugin-sonarjs.
import js from "@eslint/js";
import globals from "globals";
import sonarjs from "eslint-plugin-sonarjs";
import tseslint from "typescript-eslint";

export default tseslint.config(
{
files: ["wwwroot/**/*.js"],
extends: [js.configs.recommended, sonarjs.configs.recommended],
plugins: { "@typescript-eslint": tseslint.plugin },
languageOptions: {
parser: tseslint.parser,
ecmaVersion: 2022,
sourceType: "script",
globals: globals.browser,
parserOptions: {
project: "./tsconfig.json",
tsconfigRootDir: import.meta.dirname
}
},
rules: {
"@typescript-eslint/no-floating-promises": "error",
// Sonar's S6544 runs this rule with checksVoidReturn off: passing an async function straight to
// addEventListener is accepted, as it is here.
"@typescript-eslint/no-misused-promises": ["error", { checksVoidReturn: false }]
}
}
);
Loading
Loading