Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
7 changes: 7 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ updates:
labels:
- "dependencies"
- "dotnet"
# Cooldown: wait 7 days after a release before proposing an update, so freshly
# published packages have time to have any release-day issues surfaced by the
# ecosystem before they land in a PR here.
cooldown:
default-days: 7
ignore:
# The FsCheck.Xunit 3.x migration is done (#175). Keep holding *future major*
# bumps (4.x+) so a breaking API change to the published contract-test bases
Expand All @@ -27,6 +32,8 @@ updates:
labels:
- "dependencies"
- "github-actions"
cooldown:
default-days: 7
groups:
github-actions:
patterns:
Expand Down
18 changes: 13 additions & 5 deletions .github/workflows/codeql.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -160,14 +160,22 @@ jobs:
- name: Complete Security Scan
if: always()
shell: pwsh
env:
# Bind the workflow-context values to env vars so the PowerShell body
# reads them via $env:… — avoids template-injection into the run script
# (zizmor: template-injection).
CHECK_CSHARP_OUTCOME: ${{ steps.check-csharp.outcome }}
HAS_CSHARP: ${{ steps.check-csharp.outputs.has-csharp }}
BUILD_OUTCOME: ${{ steps.build.outcome }}
CODEQL_OUTCOME: ${{ steps.perform-codeql-analysis.outcome }}
run: |
Write-Host "=== CodeQL Security Scan Complete ===" -ForegroundColor Cyan

# Check the outcome of previous steps
$checkCsharpOutcome = "${{ steps.check-csharp.outcome }}"
$hasCsharp = "${{ steps.check-csharp.outputs.has-csharp }}"
$buildOutcome = "${{ steps.build.outcome }}"
$codeqlOutcome = "${{ steps.perform-codeql-analysis.outcome }}"
$checkCsharpOutcome = $env:CHECK_CSHARP_OUTCOME
$hasCsharp = $env:HAS_CSHARP
$buildOutcome = $env:BUILD_OUTCOME
$codeqlOutcome = $env:CODEQL_OUTCOME

# Determine overall status
$hasFailure = $false
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/integration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false

- name: Setup .NET
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
Expand Down
29 changes: 20 additions & 9 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -618,8 +618,13 @@ jobs:

- name: Verify trusted-publishing username is configured
shell: bash
env:
# Bind the workflow variable into an env var so the shell body reads
# it as "$NUGET_USER" instead of expanding ${{ vars.NUGET_USER }}
# directly into the run script (zizmor: template-injection).
NUGET_USER: ${{ vars.NUGET_USER }}
run: |
if [ -z "${{ vars.NUGET_USER }}" ]; then
if [ -z "$NUGET_USER" ]; then
echo "::error::vars.NUGET_USER is not set. Add the nuget.org account username under Settings → Secrets and variables → Actions → Variables, and create a Trusted Publishing policy on nuget.org for Chris-Wolfgang/AuditTrail / release.yaml."
exit 1
fi
Expand Down Expand Up @@ -744,12 +749,18 @@ jobs:
subject-path: './nuget-packages/*.nupkg'

- name: Attach artifacts to release
uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2
with:
tag_name: ${{ github.event.release.tag_name }}
files: |
./nuget-packages/*.nupkg
./nuget-packages/*.bom.json
release-coverage.zip
reproducible-build-manifest.json
# Uses the runner-bundled `gh` CLI instead of an external action — the
# release already exists (user-tagged; this workflow only runs on
# release:published), so `gh release upload` attaches to it directly
# (zizmor: superfluous-actions).
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: |
gh release upload "$RELEASE_TAG" \
./nuget-packages/*.nupkg \
./nuget-packages/*.bom.json \
release-coverage.zip \
reproducible-build-manifest.json \
--clobber

3 changes: 2 additions & 1 deletion .github/workflows/workflow-security.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ jobs:
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
run: zizmor --format sarif . > zizmor.sarif
# --config points at repo-root zizmor.yml (rule waivers with rationale).
run: zizmor --config zizmor.yml --format sarif . > zizmor.sarif

- name: Upload SARIF
uses: github/codeql-action/upload-sarif@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
Expand Down
54 changes: 54 additions & 0 deletions AuditTrail.slnx.DotSettings
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<!--
ReSharper InspectCode noise-floor (see issue #231).

Purpose: silence InspectCode inspections that are either (a) already covered
and gated by the build's Roslyn analyzer stack (Meziantou, SonarAnalyzer,
Roslynator, AsyncFixer, VS.Threading, BannedApiAnalyzers, .NET SDK analyzers)
— double-reporting them here adds no signal — or (b) known false positives on
this repo's multi-package / multi-TFM layout. The goal is 0 noise findings on
a clean main so the first error after the job lands is always actionable.

Only error-severity findings gate merge (pr.yaml inspectcode job); these
entries drop the corresponding warnings from Code Scanning entirely.

Follows the canonical noise-floor profile proven out in Extensions-Logging-Data
(repo-template#431 rollout).
-->

<!-- CheckNamespace: all AuditTrail library code deliberately declares the flat
root namespace `Wolfgang.AuditTrail` (plus `Wolfgang.AuditTrail.Entities` /
`.Schema` / `.Serializers`), independent of which package (Abstractions,
EntityFrameworkCore, TestKit.Xunit) the physical folder lives in. The
Polyfills/ folder additionally declares the polyfill types under their
runtime `System.*` namespace on TFMs that lack them. R# expects namespace
to mirror the folder path — a valid check on a single-project solution,
but noise for this multi-package fleet layout. -->
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CheckNamespace/@EntryIndexedValue">DO_NOT_SHOW</s:String>

<!-- Redundant-code style rules — covered in-build by Roslynator / IDE00xx and
.editorconfig. RedundantUsingDirective in particular is a FALSE POSITIVE on
src library projects: <ImplicitUsings> is enabled only for net10.0 (and
net8.0 on some projects), so explicit System usings are REQUIRED on
netstandard2.0 / net6.0 but look redundant to InspectCode (which analyses
a single TFM). -->
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantUsingDirective/@EntryIndexedValue">DO_NOT_SHOW</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantNameQualifier/@EntryIndexedValue">DO_NOT_SHOW</s:String>

<!-- Nullability — owned by the Roslyn nullable reference-type analysis, not R#.
The *AccordingTo(Nullable)APIContract rules flag defensive null-checks on
non-null-annotated parameters; those checks are INTENTIONAL in a library
that is called from nullable-oblivious code where callers can still pass
null despite the annotation. RedundantSuppressNullableWarningExpression
duplicates SonarAnalyzer S8969, which is the rule we actually gate on. -->
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantSuppressNullableWarningExpression/@EntryIndexedValue">DO_NOT_SHOW</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract/@EntryIndexedValue">DO_NOT_SHOW</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract/@EntryIndexedValue">DO_NOT_SHOW</s:String>

<!-- Public-API false positives: R# cannot see external consumers of a library's
public auto-properties or record positional properties, so it reports them
as unused (.Global variant). The NuGet-published types are the whole point
of the library — treat both rules as noise. -->
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UnusedAutoPropertyAccessor_002EGlobal/@EntryIndexedValue">DO_NOT_SHOW</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=NotAccessedPositionalProperty_002EGlobal/@EntryIndexedValue">DO_NOT_SHOW</s:String>
</wpf:ResourceDictionary>
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ private static IHostBuilder AddSingleConfigFile
bool reloadOnChange
)
{
builder.ConfigureAppConfiguration((context, configurationBuilder) =>
builder.ConfigureAppConfiguration((_, configurationBuilder) =>
{
configurationBuilder
.SetBasePath(AppContext.BaseDirectory)
Expand Down
2 changes: 1 addition & 1 deletion src/Wolfgang.AuditTrail.Cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ private static async Task<int> Main(string[] args)
.ReadFrom.Configuration(context.Configuration)
.Enrich.WithProperty("Version", Assembly.GetEntryAssembly()?.GetName().Version);
})
.ConfigureServices((context, services) =>
.ConfigureServices((_, services) =>
{
services
.AddSingleton<IReporter, ConsoleReporter>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public static void SetItem(this DbContext context, string key, object? value)

public static T? GetItem<T>(this DbContext context, string key)
{
if (_state.TryGetValue(context, out var state) && state!.TryGetValue(key, out var value))
if (_state.TryGetValue(context, out var state) && state.TryGetValue(key, out var value))
{
return (T?)value;
}
Expand All @@ -47,7 +47,7 @@ public static void RemoveItem(this DbContext context, string key)
{
if (_state.TryGetValue(context, out var state))
{
state!.Remove(key);
state.Remove(key);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public async Task DropTablesAsync(DbContext context, CancellationToken cancellat
var headerTable = EnsureSafeIdentifier(_options.HeaderTableName, nameof(_options.HeaderTableName));
var schema = string.IsNullOrWhiteSpace(_options.Schema)
? null
: EnsureSafeIdentifier(_options.Schema!, nameof(_options.Schema));
: EnsureSafeIdentifier(_options.Schema, nameof(_options.Schema));

var detailFqn = QuoteIdentifier(context.Database.ProviderName, schema, detailTable);
var headerFqn = QuoteIdentifier(context.Database.ProviderName, schema, headerTable);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ public void Null_string_round_trips()
[Fact]
public void Empty_byte_array_round_trips()
{
var decoded = RoundTrip<byte[]>(Array.Empty<byte>());
var decoded = RoundTrip(Array.Empty<byte>());
Assert.Equal(Array.Empty<byte>(), decoded);
}

Expand Down
2 changes: 1 addition & 1 deletion tests/Wolfgang.AuditTrail.Cli.Tests.Unit/MigrateTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ public async Task OnExecuteAsync_when_env_var_set_uses_resolved_value()

Assert.Equal(ExitCode.Success, exit);
Assert.NotNull(runner.CapturedOptions);
Assert.Equal("Server=.;Database=fromenv", runner.CapturedOptions!.ConnectionString);
Assert.Equal("Server=.;Database=fromenv", runner.CapturedOptions.ConnectionString);
}
finally
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,16 @@ public MappedColumnContext(DbContextOptions<MappedColumnContext> options, IAudit



// EF hydrates these via reflection; R# can't see the runtime usage.
// ReSharper disable UnusedMember.Local
// ReSharper disable UnusedAutoPropertyAccessor.Local
private sealed class MappedItem
{
public int Id { get; set; }

[Column("item_display")]
public string DisplayName { get; set; } = string.Empty;
}
// ReSharper restore UnusedAutoPropertyAccessor.Local
// ReSharper restore UnusedMember.Local
}
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ public async Task Delete_captures_OriginalValue_from_pre_save_snapshot()

// ── Fixture and entity isolated from the shared TestDbContext ───────────

// EF hydrates these via reflection; R# can't see the runtime usage.
// ReSharper disable UnusedAutoPropertyAccessor.Local
[ExcludeFromCodeCoverage]
private sealed class Widget
{
Expand All @@ -129,6 +131,7 @@ private sealed class Widget

public int RowVersion { get; set; }
}
// ReSharper restore UnusedAutoPropertyAccessor.Local



Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,17 +48,15 @@ private TestDbContext BuildContext(string? schema)
// testing can't attribute those mutants to these assertions. Force a
// fresh model per build so each test actually exercises the config.
.ReplaceService<IModelCacheKeyFactory, UncachedModelCacheKeyFactory>();
return new TestDbContext(builder.Options, new StaticAuditUserProvider("u", null), options);
return new TestDbContext(builder.Options, new StaticAuditUserProvider("u"), options);
}

// Interface-mandated signatures; parameters are intentionally unused because a
// unique key per call is exactly what disables the model cache.
// Interface-mandated signature; the parameters are intentionally unused because
// a unique key per call is exactly what disables the model cache.
#pragma warning disable RCS1163, S1172 // Unused parameter — intentional (unique key per call)
private sealed class UncachedModelCacheKeyFactory : IModelCacheKeyFactory
{
public object Create(DbContext context, bool designTime) => new object();

public object Create(DbContext context) => new object();
}
#pragma warning restore RCS1163, S1172

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ public void Serialize_primitive_types_use_invariant_formatting(object value, str
{
var sut = new PipeDelimitedEntityKeySerializer();

Assert.Equal(expected, sut.Serialize(new object?[] { value }));
Assert.Equal(expected, sut.Serialize(new[] { value }));
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ public void AuditMigrationsDbContext_when_schema_is_set_routes_version_table_und
using var ctx = new AuditMigrationsDbContext(dbOptions, auditOptions);
var entity = ctx.Model.FindEntityType(typeof(AuditSchemaVersion));
Assert.NotNull(entity);
Assert.Equal("myaudit", entity!.GetSchema());
Assert.Equal("myaudit", entity.GetSchema());
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ public sealed class StringAuditValueSerializerExactFormatTests
{
private enum Color
{
// Kept so the enum has more than one member — the encoding test only
// exercises Green, but a single-member enum would be an odd shape.
// ReSharper disable once UnusedMember.Local
Red,
Green,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public void UseAuditing_adds_AuditSaveChangesInterceptor_to_DbContextOptions()
?.Interceptors;

Assert.NotNull(interceptors);
Assert.Contains(interceptors!, i => i is AuditSaveChangesInterceptor);
Assert.Contains(interceptors, i => i is AuditSaveChangesInterceptor);
}


Expand Down
16 changes: 16 additions & 0 deletions zizmor.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# zizmor configuration — see https://docs.zizmor.sh/configuration/
#
# rules.<id>.ignore is a list of "path[:line[:column]]" globs. Findings whose
# location matches ANY entry are suppressed. Keep every waiver justified inline;
# a bare entry is a technical-debt marker with no accountability.
rules:
dangerous-triggers:
ignore:
# pr.yaml deliberately uses `pull_request_target` so gating checks run from
# the base-branch definition of pr.yaml (not from PR-authored copies), which
# is required for the coverage / analyzer / license gates to be tamper-proof
# on this workflow's design. AuditTrail has no external forked-PR
# contributors today, so the untrusted-code-checkout risk that motivates the
# zizmor warning does not apply here. Migration to `pull_request` (per the
# fleet-wide preference in CLAUDE MEMORY.md) is tracked separately.
- pr.yaml
Loading