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
134 changes: 134 additions & 0 deletions docs/nuget-quality-gate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# Core publication gate: review and migration

Tracks [#29](https://github.com/andregoepel/core/issues/29) and the source-change
evidence in [#30](https://github.com/andregoepel/core/issues/30). This change prepares
the publication path. It does not enable it or remove the existing tag publisher.

## Inventory and proposed mapping

Read-only inventory on 2026-10-23:

| Item | Observed state | Proposed state |
| --- | --- | --- |
| Package | `AndreGoepel.Core`; authenticated owner list contains only `andre.goepel` | Same package and owner |
| Publication | `ci.yml`, `pack-and-publish`, `v*` push, official NuGet/login then dotnet push | Separate verified preparation and protected publication |
| Readiness | `sonar-main.yml`, workflow 392994102 | Reuse exact successful analysis; leave this workflow unchanged |
| Required CI | `ci.yml`, workflow 320576462 | Successful exact-main run/attempt and approved workflow fingerprint |
| Repository secrets | `NUGET_USER` name present; values not read | Owner identity only; no persistent publication API key |
| Environments | `sonar-main` exists | Add `sonar-nuget-prepare` and `nuget-publish` only after approval |
| Rulesets | Default-branch protection; no release-tag ruleset observed | Immutable `v*` tags, no bypass actors |
| NuGet Trusted Publisher | One active Core policy: `andregoepel/core`, `ci.yml`, no environment restriction, exact package `AndreGoepel.Core`, push new versions only | Replace with the reviewed environment-bound Core publication policy |
| NuGet API keys | Authenticated account reports no API keys | Keep persistent API keys absent; recheck before cutover |

Public ownership source: <https://www.nuget.org/packages/AndreGoepel.Core>.
The authenticated NuGet audit confirmed the current owner list, the matching
Core Trusted Publisher policy and an empty API-key inventory. No other policy in
that account matched Core or a wildcard covering Core. No credentials were read,
created or changed. This is a point-in-time audit; repeat it before cutover. The
owner retains administrative control and can create new credentials or upload
manually, so the pipeline does not constrain future owner actions. The active
`ci.yml` policy remains a known bypass until the separately approved migration.

Proposed single policy: package `AndreGoepel.Core`; account `andre.goepel`;
GitHub owner `andregoepel`; repository `core`; workflow `nuget-publish.yml`;
environment `nuget-publish`. Restrict the production environment to `main`, require
`andregoepel` as reviewer, allow self-review for the single operator, and disallow
administrator bypass. Preparation receives only read/analysis credentials in its
own environment with a reviewed `v*` tag policy; it has no OIDC permission.

## Prepared files and shared dependency

- `scripts/sonar/publication-target.json` binds repository, readiness workflow,
exact package inventory and the independently pinned publication verifier.
- `scripts/sonar/nuget-policy.json` pins preparation workflow identity, normalized
content hash, required successful steps and a 120-minute maximum age.
- `scripts/sonar/publication/nuget-prepare.yml` and `nuget-publish.yml` are review
templates **outside `.github/workflows`**, with literal-false job guards.
- `scripts/sonar/publication/release-tags.ruleset.json` is an unapplied proposal.
- The companion [workflows PR #31](https://github.com/andregoepel/workflows/pull/31)
adapts the accepted app-foundation verifier
to consumer-supplied identities. Core does not copy its C# implementation or
add tool dependencies to the published Core library.

The verifier revision is deliberately empty and the preparation workflow ID is
zero until the companion revision has been reviewed and the standby workflow
registered. Both are rejected by the verifier. Registering a workflow, recording
its actual ID/hash and replacing the empty pin require review; never guess IDs or
reuse app-foundation's values. The publication verifier pin is separate from
`main-policy.json.toolRevision`, which continues to identify the accepted analysis
runner. This avoids changing the existing Sonar Action for this preparation.

## Verification before credentials and upload

1. Require an original, first-attempt release tag creation for the current main
revision. Reject moved/deleted tags, another main tip, changed tag objects and
a mismatched workflow revision or event.
2. Authenticate the latest completed readiness producer and its artifacts.
Validate exact CI run/attempt and mandatory jobs/steps, source and contract
hashes, accepted baseline, frozen policy/profiles, completed Sonar task,
current analysis, gate conditions and freshness. Reruns, newer producers,
changed policy or expired evidence require new validation.
3. Build and test the versioned package, compare compilation inputs with the
analyzed build, and bind both `.nupkg` and `.snupkg` to package ID, version,
source commit and content hashes. Preserve authenticated preparation origin.
4. In the protected publication job, verify the complete handoff and retain an
immutable intent artifact. Revalidate the stored intent and live evidence
before official `NuGet/login`. Revalidate the complete package batch and each
file hash immediately before `dotnet nuget push`. Record intent, individual
attempts and outcomes for investigation; do not blindly retry partial pushes.

`library-analysis-readiness` with `releaseEligible: false` is only analysis
evidence. It cannot authorize a push by itself. Preparation and verifier outputs
also remain non-publishing; authorization additionally requires the reviewed
enabled pipeline, current live checks, protected approval and scoped NuGet policy.
The C# tool cannot log in to NuGet or upload a package. Rehearsal uses an in-memory
sink; actual publication uses only NuGet/login and dotnet's native push command.

Run `bash scripts/sonar/publication/tests/outcomes.sh` on Linux with Bash, jq and
the standard GNU utilities to test the template's actual outcome-recording step
offline. It uses synthetic intent/event files, without credentials or uploads,
and checks successful single/multiple-package batches, partial outcomes and
malformed event sequences. The expected event count comes from the verified file
inventory: one `started` and one terminal event per package or symbol file.

## Controlled cutover and rollback

This sequence is proposed, not executed:

1. Review and merge the shared support and this Core preparation. Retain green
main CI and complete #30's nonempty central new-code check against the existing
accepted baseline. Do not change thresholds, exclusions or small-change policy.
2. Recheck the recorded NuGet owner/policy/API-key audit. Review the exact final
package-to-policy mapping and retire every identified alternative upload route.
3. Submit one activation preparation PR that removes only the legacy
`pack-and-publish` job, installs the new workflows with false guards, records
the merged verifier pin and registers the real preparation ID/hash. Review the
resulting CI fingerprint update; preserve all other CI behavior. Merge only
with operator approval. Do not create `v*` test tags while the old route exists.
4. Configure the reviewed environments and immutable tag rules, prove tag update
and deletion rejection using separately authorized non-release tags, then
migrate the NuGet policy. Retain the resulting read-back evidence privately.
5. Run an explicitly approved hosted rehearsal with isolated non-release tags,
no publication credentials and no upload. Test failed/pending/missing gate,
expired/wrong-revision/superseded evidence, altered artifact and successful
eligible handoff. Record exact run/attempt and verify cleanup. The production
templates require reviewed test-event adaptations for this drill; their
current `v*` triggers must not be used as test triggers.
6. Review the combined evidence and authorize the final activation separately.
Enable preparation and publication only after all bypasses are removed. First
real release is a separate explicit operation with fresh evidence and approval.

On failure, keep publication disabled, retain evidence and revalidate main. After
activation, disable the new publication path and revoke its policy if needed;
never restore the unguarded legacy route as a rollback. Published package versions
are not transactional or safely overwriteable: reconcile NuGet state and use a
new reviewed version where necessary. External evidence storage remains deferred.

## Remaining acceptance

#29 remains open until hosted integration, reviewed cutover and bypass removal
are evidenced, with a fresh private audit at cutover. Inactive templates do not enforce the current
legacy publisher. #30 remains open until the reviewed source change is merged and
the exact central main analysis shows nonzero new lines to cover, measured passing
new-code coverage, full test/coverage inventory and successful readiness. Neither
issue is closed by merging this preparation alone.
66 changes: 66 additions & 0 deletions docs/sonar-new-code.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# New-code acceptance evidence

Tracking: [#30](https://github.com/andregoepel/core/issues/30).
Status: local validation complete; exact merged-main central evidence pending.

## Reviewed source change

Both `Result.Fail(null!)` and `Result.Fail<T>(null!)` previously returned a failure
with a null error, contradicting the factories' non-null argument contract and
the `MemberNotNullWhen` annotations. Two regression tests failed before the fix.
The factories now reject null with `ArgumentNullException` naming `error`.
Valid calls retain their behavior; this does not redesign the public record
initializers or claim to prevent every manually constructed invalid state.

The production test inventory increases from six to eight, with no coverage
exclusions. Full local Windows analysis discovered and passed all eight tests,
imported coverage for `Result.cs`, reported 100% overall coverage on seven lines
to cover, zero findings and a passing gate. Local analysis:
`3241c35e-7406-41ac-95df-36188a0ba2b5`.

The full Linux analysis also passed all eight discovered tests, imported coverage
for the same source and reported 100% coverage on seven coverable lines with zero
findings. Analysis: `5b9267e7-c16b-4793-a54e-3350b6a1e098`; task:
`392f998a-a912-4512-a8ff-954c9a12406e`. Dependency audits and formatting passed.

## Isolated new-code failure and recovery

Executed on 2026-10-23 in a separate loopback-only fixture project using the same
reviewed gate thresholds and frozen Core profiles. Its own clean analysis
`42d63cb1-c27c-42b5-a6e0-37415bdcd923` was selected as its specific baseline.
The central Core project, accepted baseline and policy were not changed.

One untested coverable line was added only to an ignored source copy, then tested
in a second ignored copy. The fixture baseline remained identical across both
analyses; small-change exemption remained disabled.

| Check | Negative | Covered recovery |
| --- | --- | --- |
| New lines to cover | 1 | 1 |
| New-code coverage | 0% — failed | 100% — passed |
| Overall coverage | 87.5% — passed | 100% — passed |
| Tests discovered/passed | 8/8 | 8/8 |
| Tool exit | 1 | 0 |
| Gate | ERROR | OK |
| Analysis | `73adb702-95aa-47df-8396-b9afdd5f26f8` | `e86b1823-fb03-43d7-a44e-3861806795d6` |
| Task | `1b458850-ea38-48c1-9b6d-66e518811557` | `45c1cfbc-bff4-47d7-89f7-8199a0b351a1` |

The negative run retained a failed summary and produced no trusted readiness
receipt. The recovery remained a local analysis with `releaseEligible: false`;
it is not a central readiness run or publication authorization. No probe code is
part of the proposed source change. Raw scanner, test and metric records remain
in ignored local evidence, including the initial reproduction and both fixture
results.

## Post-merge check required before closure

After operator review and merge, validate CI and the unchanged Sonar workflow for
the exact resulting main SHA. Record the actual run IDs and attempts, task and
analysis IDs, baseline identity, discovered/passed tests, imported coverage,
nonzero `new_lines_to_cover`, measured `new_coverage` and all applicable new-code
conditions. Verify a successful fresh readiness receipt for that exact attempt.
Do not substitute this local report or overall coverage for the central result.

Keep the accepted specific baseline, frozen profiles, thresholds, small-change
setting and exclusions unchanged. If another merge or rerun supersedes the
evidence, repeat the exact-main check instead of accepting a stale receipt.
8 changes: 6 additions & 2 deletions docs/sonar.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,24 @@ Tracking: [core #24](https://github.com/andregoepel/core/issues/24),
[epic #238](https://github.com/andregoepel/app-foundation/issues/238).

Core uses opt-in analysis through the shared C# runner in `workflows`.
The library and its dependencies, existing CI and tag publisher are unchanged.
The first wave left the library, its dependencies, existing CI and tag publisher unchanged.
Central main analysis is enabled with `SONAR_MAIN_ENABLED=true`. The initial
baseline was accepted and selected on October 23, 2026. There is no package
upload, release tag, publisher migration or deployment in this wave.
The existing tag publisher is **not Sonar-protected** by this addition.

Follow-up preparation for #29/#30 is documented in
[the publication migration plan](nuget-quality-gate.md) and
[the new-code evidence report](sonar-new-code.md).

## Reviewed scope

| Item | Inventory |
| --- | --- |
| Solution | `AndreGoepel.Core.slnx` |
| Production project | `src/AndreGoepel.Core/AndreGoepel.Core.csproj` |
| Test project | `tests/AndreGoepel.Core.Tests/AndreGoepel.Core.Tests.csproj` |
| Current tests | Six; discovery must find at least six and all discovered tests must pass |
| Current tests | Eight; discovery must find at least eight and all discovered tests must pass |
| Executed coverage anchor | `src/AndreGoepel.Core/Result.cs` |
| SDK / scanner / coverage | 10.0.402 / 12.0.0 / 18.12.0 |
| Sonar Community Build | 26.10.0.133078 |
Expand Down
2 changes: 1 addition & 1 deletion scripts/sonar/contract.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"centralServerId": "AA2DD71C-AaFDycIuQLOXJXX6YWNZ",
"gate": "Core Library 1.0.0",
"solution": "AndreGoepel.Core.slnx",
"tests": { "AndreGoepel.Core.Tests": 6 },
"tests": { "AndreGoepel.Core.Tests": 8 },
"coverageAnchors": ["src/AndreGoepel.Core/Result.cs"],
"sourceExclusions": ["**/bin/**", "**/obj/**", "**/.sonarqube/**", "artifacts/**"]
}
8 changes: 8 additions & 0 deletions scripts/sonar/nuget-policy.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"workflowId": 0,
"workflowSha256": "",
"maximumAgeMinutes": 120,
"jobs": {
"prepare": ["Verify tag push", "Prepare NuGet batch", "Publish package batch", "Publish origin"]
}
}
6 changes: 6 additions & 0 deletions scripts/sonar/publication-target.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"repository": "andregoepel/core",
"readinessWorkflowId": 392994102,
"packages": ["AndreGoepel.Core"],
"verifierRevision": ""
}
Loading
Loading