Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
b35e495
🔄 update ci/cd workflow references and codecov branch
gimlichael Sep 23, 2026
480adce
🔄 update contributing guide with new ci/cd workflow structure
gimlichael Sep 23, 2026
bc6c348
🗑️ remove obsolete ci-pipeline workflow file
gimlichael Sep 23, 2026
9d3be7d
📝 add .bot workspace readme with ai working guidelines
gimlichael Sep 23, 2026
af5491b
🤖 add .bot workspace to gitignore with readme exception
gimlichael Sep 23, 2026
d5859e8
🧪 expand servicecollectionextensions test coverage with comprehensive…
gimlichael Oct 2, 2026
271b734
✨ add configured options integration
aicia-bot Oct 2, 2026
130b6b4
📝 document configured options api behavior
aicia-bot Oct 2, 2026
0caea60
📦 add configured options package notes
aicia-bot Oct 2, 2026
3fb3bd1
💬 add 10.8.0 changelog entry
gimlichael Oct 2, 2026
38db817
📝 update CI runner guidance
aicia-bot Oct 2, 2026
de455a7
👷 update GitHub workflow runners
aicia-bot Oct 2, 2026
f02e921
🐛 propagate cancellation during options validation
aicia-bot Oct 2, 2026
9f8e43c
🐛 ensure unmanaged cleanup after managed cleanup fails
aicia-bot Oct 3, 2026
cf06640
⬆️ update coverlet and sqlclient versions
aicia-bot Oct 3, 2026
10a1a65
👷 reuse the shared codecov workflow
aicia-bot Oct 3, 2026
9b8e9fd
📦 update package release notes
aicia-bot Oct 3, 2026
0bcd6c2
💬 update 10.8.0 release summary
aicia-bot Oct 3, 2026
d56d25a
✅ use UTC validity windows in signed URI test
aicia-bot Oct 3, 2026
98908e9
✅ isolate latency limits from retry behavior tests
aicia-bot Oct 3, 2026
29f2945
original
gimlichael Oct 3, 2026
8a4d295
👷 switch release automation to validated version tags
aicia-bot Oct 3, 2026
83de72a
👷 add published release deployment workflow
aicia-bot Oct 3, 2026
1950469
💬 update 10.8.0 changelog with release details
aicia-bot Oct 3, 2026
79f1588
🐛 preserve exceptions from both disposal hooks
aicia-bot Oct 3, 2026
26d1890
🔒️ verify the live release tag before publishing
aicia-bot Oct 3, 2026
e7ef743
🔧 pass Codecov tokens explicitly to reusable workflows
aicia-bot Oct 3, 2026
eb5cd84
💬 document release and deployment steps
aicia-bot Oct 3, 2026
fca2b1d
update cl
gimlichael Oct 3, 2026
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
10 changes: 10 additions & 0 deletions .bot/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# .bot Workspace

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Tracked bot workspace guide

This adds a tracked guide for a local-only bot workspace. The repository’s public-API documentation directive limits new working-tree files to the managed AGENTS block, active DocFX configuration, an approved waiver, or DocFX namespace and type pages. This file is outside those categories, so the repository requirement must be satisfied before merging.

Context Used: AGENTS.md (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: .bot/README.md
Line: 1

Comment:
**Tracked bot workspace guide**

This adds a tracked guide for a local-only bot workspace. The repository’s public-API documentation directive limits new working-tree files to the managed AGENTS block, active DocFX configuration, an approved waiver, or DocFX namespace and type pages. This file is outside those categories, so the repository requirement must be satisfied before merging.

**Context Used:** AGENTS.md ([source](https://github.com/codebeltnet/cuemon/blob/main/AGENTS.md))

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Codex


This folder is reserved for local-only AI working material such as:

- brainstorm notes
- draft implementation plans
- design alternatives
- temporary agent state

Keep this folder out of source control. Move only finalized, non-confidential guidance into `AGENTS.md` or `.github/copilot-instructions.md`.
10 changes: 8 additions & 2 deletions .docfx/api/namespaces/Cuemon.Extensions.DependencyInjection.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@
uid: Cuemon.Extensions.DependencyInjection
summary: *content
---
Register services in the Microsoft DI container with or without options, specifying service and implementation types through a rich set of generic extension methods. Use this namespace when you need flexible DI registration with typed options. Start with `Add<TService, TImplementation>` for basic registration, or `Add<TService, TImplementation, TOptions>` when your service requires configuration options.
Register service contracts and typed configuration in Microsoft's dependency injection container. Use `Add<TService, TImplementation>` to select a service implementation and lifetime, or `Add<TService, TImplementation, TOptions>` to also register its configuration using the existing first-configuration convention. Choose `AddConfiguredOptions<TOptions>` when consumers need Cuemon Parameter Object conventions within the Microsoft Options lifecycle, together with direct options and configurator injection.

Call `AddConfiguredOptions<TOptions>` when an options type implements Cuemon's `IParameterObject` conventions and needs to participate in the [Microsoft Options pattern](https://learn.microsoft.com/en-us/dotnet/core/extensions/options). Microsoft constructs the options and runs configuration, post-configuration, and validation. Cuemon's `IPostConfigurableParameterObject.PostConfigureOptions()` and `IValidatableParameterObject.ValidateOptions()` participate in the corresponding stages for every options name. Recoverable validation exceptions become `OptionsValidationException` failures when options are materialized; post-configuration exceptions and fatal validation exceptions propagate without translation.

Direct `TOptions` consumption resolves the same cached default instance as `IOptions<TOptions>.Value`, with singleton semantics. Use `IOptionsSnapshot<TOptions>` for scoped snapshots and `IOptionsMonitor<TOptions>` for named options, invalidation, and change notifications; their Microsoft lifecycles remain independent of direct consumption. Post-configurators and validators execute in registration order within their respective stages, so Cuemon's conventions are not guaranteed to run last.

The first `AddConfiguredOptions<TOptions>` call registers the primary default configurator and exposes that exact delegate as `Action<TOptions>`. Later calls for the same type are ignored. Ordinary `Configure<TOptions>` registrations before or after it still compose through Microsoft Options, but do not become part of the injectable delegate. Invoking that delegate against a fresh object applies only the primary configuration, without post-configuration or validation. `TryConfigure<TOptions>` retains its existing first-configuration semantics, and `PostConfigureAllOf<TOptions>` can still add bulk post-configuration for compatible options registrations.

[!INCLUDE [availability-default](../../includes/availability-default.md)]

Expand All @@ -12,6 +18,6 @@ Complements: [Microsoft.Extensions.DependencyInjection namespace](https://docs.m

|Type|Ext|Methods|
|--:|:-:|---|
|IServiceCollection|⬇️|`Add`, `Add<TService>`, `Add<TOptions>`, `Add<TService, TImplementation>`, `Add<TService, TImplementation, TOptions>`, `TryAdd`, `TryAdd<TService>`, `TryAdd<TOptions>`, `TryAdd<TService, TImplementation>`, `TryAdd<TService, TImplementation, TOptions>`, `TryConfigure<TOptions>`, `PostConfigureAllOf<TOptions>`|
|IServiceCollection|⬇️|`Add`, `Add<TService>`, `Add<TOptions>`, `Add<TService, TImplementation>`, `Add<TService, TImplementation, TOptions>`, `TryAdd`, `TryAdd<TService>`, `TryAdd<TOptions>`, `TryAdd<TService, TImplementation>`, `TryAdd<TService, TImplementation, TOptions>`, `TryConfigure<TOptions>`, `AddConfiguredOptions<TOptions>`, `PostConfigureAllOf<TOptions>`|
|IServiceProvider|⬇️|`GetServiceDescriptors`|
|type|⬇️|`TryGetDependencyInjectionMarker`|
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,65 @@ namespace Cuemon.Docs.Samples.DependencyInjection
}
}
```

Configure delivery retries with `AddConfiguredOptions<DeliveryOptions>` to let Microsoft Options construct the Parameter Object, calculate its retry budget during post-configuration, and validate the final settings. This example adds an ordinary Microsoft configurator that raises the attempt limit to four, then resolves the cached default options directly and through `IOptions<DeliveryOptions>`. The output shows a 15-second retry budget and a shared default instance. The injectable `Action<DeliveryOptions>` is the exact primary delegate and sets three attempts on a fresh object; invoking it directly does not calculate the budget or validate the object. Further `AddConfiguredOptions<DeliveryOptions>` calls would be ignored, while ordinary `Configure` calls still compose. Cuemon's conventions participate for all options names in registration order, and recoverable validation failures surface as `OptionsValidationException` when Microsoft materializes options.

```csharp
using System;
using Cuemon.Configuration;
using Cuemon.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;

namespace Delivery.Configuration;

public static class DeliveryApplication
{
public static void Main()
{
Action<DeliveryOptions> setup = options =>
{
options.MaxAttempts = 3;
options.RetryDelay = TimeSpan.FromSeconds(5);
};
var services = new ServiceCollection();
services.AddConfiguredOptions(setup);
services.Configure<DeliveryOptions>(options => options.MaxAttempts = 4);

using (var provider = services.BuildServiceProvider())
{
var delivery = provider.GetRequiredService<DeliveryOptions>();
var microsoftOptions = provider.GetRequiredService<IOptions<DeliveryOptions>>().Value;
Console.WriteLine($"Attempts: {delivery.MaxAttempts}; retry budget: {delivery.RetryBudget.TotalSeconds} seconds");
Console.WriteLine($"Shared default instance: {ReferenceEquals(delivery, microsoftOptions)}");

var configure = provider.GetRequiredService<Action<DeliveryOptions>>();
var fresh = new DeliveryOptions();
configure(fresh);
Console.WriteLine($"Primary configurator attempts: {fresh.MaxAttempts}");
}
}
}

public sealed class DeliveryOptions : IPostConfigurableParameterObject, IValidatableParameterObject
{
public int MaxAttempts { get; set; }

public TimeSpan RetryDelay { get; set; }

public TimeSpan RetryBudget { get; private set; }

public void PostConfigureOptions()
{
RetryBudget = TimeSpan.FromTicks(RetryDelay.Ticks * (MaxAttempts - 1));
}

public void ValidateOptions()
{
if (MaxAttempts < 1 || RetryDelay < TimeSpan.Zero)
{
throw new InvalidOperationException("Delivery requires at least one attempt and a nonnegative retry delay.");
}
}
}
```
14 changes: 13 additions & 1 deletion .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ This repository is part of the Codebelt .NET library estate. The instructions be
- `src/` contains production projects.
- `test/` contains xUnit v3 test projects.
- `Cuemon.slnx` is the solution used for local development.
- `.github/workflows/ci-pipeline.yml` is the CI workflow and the authority for the test matrix.
- `.github/workflows/pr.yml` owns the PR test matrix and blocking quality gates.
- `.github/workflows/release.yml` publishes packages from a version-tagged commit in `main` history; post-release assurance and DocFX production run after NuGet publication.
- `.github/workflows/deploy.yml` promotes the published DocFX image without rebuilding it. See [Release and deployment](#release-and-deployment) for the CI/CD handoff.
- `testenvironments.json` declares the supported `WSL-Ubuntu` and `Docker-Ubuntu` test environments.

## Build
Expand Down Expand Up @@ -69,6 +71,16 @@ dotnet pack "Cuemon.slnx" --configuration Release --no-restore

Package-specific release notes live under `.nuget/<ProjectName>/PackageReleaseNotes.txt` and package README files live beside them. `Directory.Build.targets` imports the release notes during packing. Public API changes also require XML documentation updates; DocFX documentation is built by the repository automation.

## Release and deployment

After PR validation and merge, a maintainer creates and pushes a `vX.Y.Z` tag (or `vX.Y.Z-prerelease`, without build metadata) for the intended commit in `main` history. The tag push starts `release.yml`, which checks the tag identity and ancestry, builds signed Release packages from that commit, validates their versions and existing NuGet content, and sends the validated package artifact to the protected `Production` publication job.

After NuGet publication, the workflow runs post-release tests and analysis and builds the multi-platform DocFX OCI image from the same commit. The verified archive and SHA-256 checksum are attached to a draft GitHub Release before that release is published. Post-release assurance failures do not roll back published packages; inspect the release summary and resolve failures against its recorded commit.

A published GitHub Release starts `deploy.yml`. To retry deployment, dispatch that workflow from `main` with the existing published release tag. Deployment requires the versioned OCI archive and checksum, resolves the tag to its source commit, and promotes the verified image to JCR through `Production` without rebuilding it. The workflow reports the immutable image digest for a Kubernetes handoff; this repository does not perform the Kubernetes rollout.

If publication fails, inspect the job results before retrying. A partially completed NuGet push may already have published some packages; rerun the failed publication job to reuse its validated artifact. Keep release tags fixed: publication rechecks the live tag against the built commit and rejects a mismatch.

## Pull requests

1. Create or join an issue before substantial work, then fork the repository and create a branch from `main`.
Expand Down
172 changes: 172 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
name: Deploy Flow
on:
release:
types: [published]
workflow_dispatch:
inputs:
tag:
type: string
description: Existing published GitHub Release tag to deploy, e.g. v10.7.2.
required: true

permissions:
contents: read

concurrency:
group: cuemon-deploy-${{ github.event.release.tag_name || inputs.tag }}
cancel-in-progress: false

jobs:
resolve_release:
name: Resolve published release and authoritative SHA
runs-on: ubuntu-26.04
permissions:
contents: read
outputs:
version: ${{ steps.resolve.outputs.version }}
tag: ${{ steps.resolve.outputs.tag }}
sha: ${{ steps.resolve.outputs.sha }}
steps:
- id: resolve
name: Validate release identity and resolve the released commit
shell: bash
env:
GH_TOKEN: ${{ github.token }}
WORKFLOW_REF: ${{ github.ref }}
RELEASE_TAG: ${{ github.event.release.tag_name || inputs.tag }}
run: |
set -euo pipefail

if [[ "$GITHUB_EVENT_NAME" == "release" ]]; then
if ! jq -e --arg tag "$RELEASE_TAG" '.action == "published" and .release.draft == false and .release.tag_name == $tag' "$GITHUB_EVENT_PATH" >/dev/null; then
echo "::error::Deployment requires a published, non-draft GitHub Release matching '$RELEASE_TAG'."
exit 1
fi
elif [[ "$GITHUB_EVENT_NAME" == "workflow_dispatch" ]]; then
if [[ "$WORKFLOW_REF" != "refs/heads/main" ]]; then
echo "::error::Deployment dispatch must target refs/heads/main; received '$WORKFLOW_REF'."
exit 1
fi
else
echo "::error::Unsupported deployment event '$GITHUB_EVENT_NAME'."
exit 1
fi

if [[ "$RELEASE_TAG" != v* ]]; then
echo "::error::Release tag '$RELEASE_TAG' must have the v prefix (for example, v10.7.2)."
exit 1
fi

RELEASE_VERSION="${RELEASE_TAG#v}"
semver_regex='^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9][0-9]*|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*))*))?$'
if [[ ! "$RELEASE_VERSION" =~ $semver_regex ]]; then
echo "::error::'$RELEASE_VERSION' is not a supported SemVer release version."
exit 1
fi

release_tag="$RELEASE_TAG"
release_json="$(gh api "repos/$GITHUB_REPOSITORY/releases/tags/$release_tag")"
if ! jq -e --arg tag "$release_tag" '.tag_name == $tag and .draft == false and (.published_at | type == "string")' <<< "$release_json" >/dev/null; then
echo "::error::GitHub Release '$release_tag' is missing, has a different tag, or is not published."
exit 1
fi

archive_name="cuemon-docfx-$RELEASE_VERSION.oci.tar"
checksum_name="$archive_name.sha256"
if ! jq -e --arg archive "$archive_name" --arg checksum "$checksum_name" \
'any(.assets[]; .name == $archive and .state == "uploaded") and any(.assets[]; .name == $checksum and .state == "uploaded")' <<< "$release_json" >/dev/null; then
echo "::error::GitHub Release '$release_tag' must contain the immutable OCI archive '$archive_name' and checksum '$checksum_name'."
exit 1
fi

ref_json="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$release_tag")"
object_type="$(jq -r '.object.type' <<< "$ref_json")"
object_sha="$(jq -r '.object.sha' <<< "$ref_json")"
if [[ "$object_type" == "tag" ]]; then
tag_json="$(gh api "repos/$GITHUB_REPOSITORY/git/tags/$object_sha")"
object_type="$(jq -r '.object.type' <<< "$tag_json")"
object_sha="$(jq -r '.object.sha' <<< "$tag_json")"
fi

if [[ "$object_type" != "commit" || ! "$object_sha" =~ ^[0-9a-fA-F]{40}$ ]]; then
echo "::error::Release tag '$release_tag' does not resolve to a Git commit."
exit 1
fi

{
echo "version=$RELEASE_VERSION"
echo "tag=$release_tag"
echo "sha=$object_sha"
} >> "$GITHUB_OUTPUT"

{
echo "## DocFX promotion request validated"
echo
echo "- Version: $RELEASE_VERSION"
echo "- Release tag: $release_tag"
echo "- Released SHA: $object_sha"
} >> "$GITHUB_STEP_SUMMARY"

promote_docfx_image:
name: Promote the released DocFX OCI image to JCR
needs: [resolve_release]
runs-on: ubuntu-26.04
timeout-minutes: 30
environment: Production
permissions:
contents: read
steps:
- name: Download the immutable OCI assets from the GitHub Release
shell: bash
env:
GH_TOKEN: ${{ github.token }}
RELEASE_VERSION: ${{ needs.resolve_release.outputs.version }}
RELEASE_TAG: ${{ needs.resolve_release.outputs.tag }}
run: |
set -euo pipefail
release_json="$(gh api "repos/$GITHUB_REPOSITORY/releases/tags/$RELEASE_TAG")"
if ! jq -e --arg tag "$RELEASE_TAG" '.tag_name == $tag and .draft == false and (.published_at | type == "string")' <<< "$release_json" >/dev/null; then
echo "::error::GitHub Release '$RELEASE_TAG' is no longer published."
exit 1
fi

artifact_directory="$RUNNER_TEMP/docfx-release-asset"
mkdir -p "$artifact_directory"
archive_name="cuemon-docfx-$RELEASE_VERSION.oci.tar"
checksum_name="$archive_name.sha256"
gh release download "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --pattern "$archive_name" --dir "$artifact_directory"
gh release download "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --pattern "$checksum_name" --dir "$artifact_directory"

- id: publish
name: Promote the verified OCI artifact to JCR and confirm its digest
uses: codebeltnet/oci-artifact-publish@v1
with:
archive-path: ${{ runner.temp }}/docfx-release-asset/cuemon-docfx-${{ needs.resolve_release.outputs.version }}.oci.tar
checksum-path: ${{ runner.temp }}/docfx-release-asset/cuemon-docfx-${{ needs.resolve_release.outputs.version }}.oci.tar.sha256
version: ${{ needs.resolve_release.outputs.version }}
revision: ${{ needs.resolve_release.outputs.sha }}
repository: jcr.codebelt.net/geekle/cuemon-docfx
username: ${{ secrets.JCR_USERNAME }}
password: ${{ secrets.JCR_PASSWORD }}

- name: Record the immutable JCR identity and Kubernetes handoff
shell: bash
env:
RELEASE_VERSION: ${{ needs.resolve_release.outputs.version }}
RELEASE_SHA: ${{ needs.resolve_release.outputs.sha }}
IMAGE: ${{ steps.publish.outputs.image }}
DIGEST: ${{ steps.publish.outputs.digest }}
IMAGE_BY_DIGEST: ${{ steps.publish.outputs.image-by-digest }}
run: |
set -euo pipefail
{
echo "## DocFX image promoted"
echo
echo "- Release: $RELEASE_VERSION"
echo "- Released SHA: $RELEASE_SHA"
echo "- Registry tag: $IMAGE"
echo "- Immutable registry digest: $DIGEST"
echo "- Kubernetes-ready image reference: $IMAGE_BY_DIGEST"
echo
echo "JCR publication succeeded. This POC stops at the immutable registry reference; Kubernetes rollout configuration is not present in this repository."
} >> "$GITHUB_STEP_SUMMARY"
Loading
Loading