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
7 changes: 7 additions & 0 deletions .github/workflows/publish-attested.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ name: Publish Attested

on:
workflow_dispatch:
inputs:
version:
description: Release version without the leading "v"
required: false
type: string

permissions:
contents: write
Expand All @@ -12,6 +17,8 @@ permissions:
jobs:
publish:
uses: ./.github/workflows/publish-artifacts.yml
with:
package_version: ${{ inputs.version }}

release:
name: Upload artifacts to draft release
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/release-drafter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ jobs:
publish:
needs: update-release-draft
uses: ./.github/workflows/publish-artifacts.yml
with:
package_version: ${{ needs.update-release-draft.outputs.tag_name }}

upload-release-assets:
name: Upload artifacts to release draft
Expand Down
2 changes: 1 addition & 1 deletion Docs/Decision/Adr/ADR_025_Governance_Approval_Workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
#adr_025

## Status
Proposed
Accepted

## Date
2026-06-22
Expand Down
14 changes: 14 additions & 0 deletions Examples/Governance/ApprovalWorkflow/ApprovalWorkflow.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\src\ModularityKit.Mutator.Governance.csproj" />
</ItemGroup>

</Project>
3 changes: 3 additions & 0 deletions Examples/Governance/ApprovalWorkflow/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using ApprovalWorkflow.Scenarios;

await GovernanceApprovalWorkflowScenario.Run();
23 changes: 23 additions & 0 deletions Examples/Governance/ApprovalWorkflow/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Governance ApprovalWorkflow

This example shows the governance approval workflow built on top of `MutationRequest.PendingApproval(...)` and `MutationRequestApprovalWorkflowManager`.

It demonstrates:

- mapping `PolicyRequirement` into request-level approval requirements
- multi-actor approvals in the same step
- ordered approval steps
- transition from `Pending` to `Approved` after the final approval

## Key files

- [`Program.cs`](Program.cs)
- [`Scenarios/GovernanceApprovalWorkflowScenario.cs`](Scenarios/GovernanceApprovalWorkflowScenario.cs)
- [`src/Governance/Runtime/Approval/MutationRequestApprovalWorkflowManager.cs`](../../../src/Governance/Runtime/Approval/MutationRequestApprovalWorkflowManager.cs)
- [`src/Governance/Abstractions/Approval/MutationApprovalRequirement.cs`](../../../src/Governance/Abstractions/Approval/MutationApprovalRequirement.cs)

## Run

```bash
dotnet run --project Examples/Governance/ApprovalWorkflow/ApprovalWorkflow.csproj
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
using ModularityKit.Mutator.Abstractions.Context;
using ModularityKit.Mutator.Abstractions.Intent;
using ModularityKit.Mutator.Abstractions.Policies;
using ModularityKit.Mutator.Governance.Abstractions.Requests.Model;
using ModularityKit.Mutator.Governance.Runtime.Approval.Execution;
using ModularityKit.Mutator.Governance.Runtime.Storage;

namespace ApprovalWorkflow.Scenarios;

internal static class GovernanceApprovalWorkflowScenario
{
public static async Task Run()
{
var store = new InMemoryMutationRequestStore();
var manager = new MutationRequestApprovalWorkflowManager(store);

PrintSection("Submit Pending Approval Request");
var request = await store.Create(CreateApprovalRequest());
PrintRequest(request);

PrintSection("Approve Step 1");
var aliceApproval = request.ApprovalRequirements.Single(requirement => requirement.ApproverId == "alice");
var afterAlice = await manager.ApproveRequirement(
request.RequestId,
aliceApproval.ApprovalId,
MutationContext.User("alice", "Alice", "Manager approved"));
PrintRequest(afterAlice);

PrintSection("Approve Step 1 - Second Actor");
var bobApproval = afterAlice.ApprovalRequirements.Single(requirement => requirement.ApproverId == "bob");
var afterBob = await manager.ApproveRequirement(
request.RequestId,
bobApproval.ApprovalId,
MutationContext.User("bob", "Bob", "Security approved"));
PrintRequest(afterBob);

PrintSection("Approve Step 2");
var carolApproval = afterBob.ApprovalRequirements.Single(requirement => requirement.ApproverId == "carol");
var afterCarol = await manager.ApproveRequirement(
request.RequestId,
carolApproval.ApprovalId,
MutationContext.User("carol", "Carol", "Finance approved"));
PrintRequest(afterCarol);
}

private static MutationRequest CreateApprovalRequest()
{
return MutationRequest.PendingApproval(
stateId: "tenant-42:roles",
stateType: "IamRoleState",
mutationType: "GrantRoleMutation",
intent: new MutationIntent
{
OperationName = "GrantRole",
Category = "Security",
Description = "Grant elevated role to tenant operator"
},
context: MutationContext.User("requester", "Requester", "Need elevated access for incident"),
requirements:
[
PolicyRequirement.Approval("alice", "Manager approval"),
new PolicyRequirement
{
Type = "Approval",
Description = "Security review",
Data = new
{
Approver = "bob",
StepOrder = 1,
Reason = "Security sign-off"
}
},
new PolicyRequirement
{
Type = "Approval",
Description = "Finance review",
Data = new
{
Approver = "carol",
StepOrder = 2,
Reason = "Budget sign-off"
}
}
],
expectedStateVersion: "v10");
}

private static void PrintSection(string title)
{
Console.WriteLine();
Console.WriteLine($"=== {title} ===");
}

private static void PrintRequest(MutationRequest request)
{
Console.WriteLine($"Request status: {request.Status}");
Console.WriteLine($"Pending reason: {request.PendingReason?.ToString() ?? "-"}");
Console.WriteLine($"Revision: {request.Revision}");
Console.WriteLine("Approval requirements:");

foreach (var requirement in request.ApprovalRequirements.OrderBy(requirement => requirement.StepOrder).ThenBy(requirement => requirement.ApproverId))
{
Console.WriteLine(
$" - Step {requirement.StepOrder}: {requirement.ApproverId} => {requirement.Status}");
}

var lastDecision = request.Decisions[^1];
Console.WriteLine($"Last decision: {lastDecision.Type} by {lastDecision.Context.ActorId ?? "system"}");
Console.WriteLine($"Reason: {lastDecision.Reason ?? "-"}");
}
}
14 changes: 14 additions & 0 deletions Examples/Governance/DecisionTaxonomy/DecisionTaxonomy.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\src\ModularityKit.Mutator.Governance.csproj" />
</ItemGroup>

</Project>
3 changes: 3 additions & 0 deletions Examples/Governance/DecisionTaxonomy/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using DecisionTaxonomy.Scenarios;

GovernanceDecisionTaxonomyScenario.Run();
29 changes: 29 additions & 0 deletions Examples/Governance/DecisionTaxonomy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Governance DecisionTaxonomy

This example shows why governance request decisions are split into separate categories instead of being kept in one flat enum.

It demonstrates:

- lifecycle decisions such as `Submitted` and `Approved`
- approval decisions such as `Requested`, `Granted`, and `Rejected`
- version-resolution decisions such as `Validated` and `RejectedAsStale`
- the shared `MutationRequestDecisionType` wrapper with:
- `Category`
- `Code`
- `ToString()`

## Key files

- [`Program.cs`](Program.cs)
- [`Scenarios/GovernanceDecisionTaxonomyScenario.cs`](Scenarios/GovernanceDecisionTaxonomyScenario.cs)
- [`src/Governance/Abstractions/Requests/Decisions/MutationRequestDecisionType.cs`](../../../src/Governance/Abstractions/Requests/Decisions/MutationRequestDecisionType.cs)
- [`src/Governance/Abstractions/Requests/Decisions/MutationRequestDecisionCategory.cs`](../../../src/Governance/Abstractions/Requests/Decisions/MutationRequestDecisionCategory.cs)
- [`src/Governance/Abstractions/Requests/Decisions/MutationRequestLifecycleDecisionType.cs`](../../../src/Governance/Abstractions/Requests/Decisions/MutationRequestLifecycleDecisionType.cs)
- [`src/Governance/Abstractions/Requests/Decisions/MutationRequestApprovalDecisionType.cs`](../../../src/Governance/Abstractions/Requests/Decisions/MutationRequestApprovalDecisionType.cs)
- [`src/Governance/Abstractions/Requests/Decisions/MutationRequestVersionResolutionDecisionType.cs`](../../../src/Governance/Abstractions/Requests/Decisions/MutationRequestVersionResolutionDecisionType.cs)

## Run

```bash
dotnet run --project Examples/Governance/DecisionTaxonomy/DecisionTaxonomy.csproj
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using ModularityKit.Mutator.Abstractions.Context;
using ModularityKit.Mutator.Governance.Abstractions.Requests.Decisions;

namespace DecisionTaxonomy.Scenarios;

internal static class GovernanceDecisionTaxonomyScenario
{
public static void Run()
{
PrintSection("Lifecycle Decisions");
PrintDecision(MutationRequestDecision.Create(
MutationRequestDecisionType.Lifecycle(MutationRequestLifecycleDecisionType.Submitted),
MutationContext.User("requester", "Requester", "Submit request"),
reason: "Request was submitted into governance."));
PrintDecision(MutationRequestDecision.Create(
MutationRequestDecisionType.Lifecycle(MutationRequestLifecycleDecisionType.Approved),
MutationContext.User("system", "System", "Request reached executable state"),
reason: "Request is now approved for execution."));

PrintSection("Approval Decisions");
PrintDecision(MutationRequestDecision.Create(
MutationRequestDecisionType.Approval(MutationRequestApprovalDecisionType.Requested),
MutationContext.User("requester", "Requester", "Approval needed for sensitive change"),
reason: "Sensitive change requires explicit sign-off."));
PrintDecision(MutationRequestDecision.Create(
MutationRequestDecisionType.Approval(MutationRequestApprovalDecisionType.Granted),
MutationContext.User("alice", "Alice", "Manager approved"),
reason: "Manager granted the required approval."));
PrintDecision(MutationRequestDecision.Create(
MutationRequestDecisionType.Approval(MutationRequestApprovalDecisionType.Rejected),
MutationContext.User("bob", "Bob", "Security rejected"),
reason: "Security review rejected the request."));

PrintSection("Version Resolution Decisions");
PrintDecision(MutationRequestDecision.Create(
MutationRequestDecisionType.VersionResolution(MutationRequestVersionResolutionDecisionType.Validated),
MutationContext.User("approver", "Approver", "Version still matches"),
reason: "Current state version still matches the approved request."));
PrintDecision(MutationRequestDecision.Create(
MutationRequestDecisionType.VersionResolution(MutationRequestVersionResolutionDecisionType.RejectedAsStale),
MutationContext.User("approver", "Approver", "State drift invalidated the request"),
reason: "Request was rejected because the approved version is stale."));
PrintDecision(MutationRequestDecision.Create(
MutationRequestDecisionType.VersionResolution(MutationRequestVersionResolutionDecisionType.RenewedApprovalRequired),
MutationContext.User("approver", "Approver", "Re-approval required on latest state"),
reason: "Request must be approved again on the latest state version."));
}

private static void PrintSection(string title)
{
Console.WriteLine();
Console.WriteLine($"=== {title} ===");
}

private static void PrintDecision(MutationRequestDecision decision)
{
Console.WriteLine($"Category: {decision.Type.Category}");
Console.WriteLine($"Code: {decision.Type.Code}");
Console.WriteLine($"Display: {decision.Type}");
Console.WriteLine($"Actor: {decision.Context.ActorId ?? "system"}");
Console.WriteLine($"Reason: {decision.Reason ?? "-"}");
Console.WriteLine();
}
}
2 changes: 1 addition & 1 deletion Examples/Governance/RequestLifecycle/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ It focuses on `MutationRequest`, `IMutationRequestStore`, and `MutationRequestLi

- [`Program.cs`](Program.cs)
- [`Scenarios/GovernanceRequestLifecycleScenario.cs`](Scenarios/GovernanceRequestLifecycleScenario.cs)
- [`src/Governance/Abstractions/Requests/MutationRequest.cs`](../../../src/Governance/Abstractions/Requests/MutationRequest.cs)
- [`src/Governance/Abstractions/Requests/Model/MutationRequest.cs`](../../../src/Governance/Abstractions/Requests/Model/MutationRequest.cs)
- [`src/Governance/Abstractions/Lifecycle/IMutationRequestLifecycleManager.cs`](../../../src/Governance/Abstractions/Lifecycle/IMutationRequestLifecycleManager.cs)
- [`src/Governance/Runtime/MutationRequestLifecycleManager.cs`](../../../src/Governance/Runtime/MutationRequestLifecycleManager.cs)
- [`src/Governance/Runtime/InMemoryMutationRequestStore.cs`](../../../src/Governance/Runtime/InMemoryMutationRequestStore.cs)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
using ModularityKit.Mutator.Abstractions.Context;
using ModularityKit.Mutator.Abstractions.Intent;
using ModularityKit.Mutator.Abstractions.Policies;
using ModularityKit.Mutator.Governance.Abstractions.Lifecycle;
using ModularityKit.Mutator.Governance.Abstractions.Requests;
using ModularityKit.Mutator.Governance.Runtime.Lifecycle;
using ModularityKit.Mutator.Governance.Abstractions.Lifecycle.Model;
using ModularityKit.Mutator.Governance.Abstractions.Requests.Model;
using ModularityKit.Mutator.Governance.Runtime.Lifecycle.Execution;
using ModularityKit.Mutator.Governance.Runtime.Storage;

namespace RequestLifecycle.Scenarios;
Expand Down
8 changes: 4 additions & 4 deletions Examples/Governance/VersionedResolution/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ It is the direct runnable example for the semantics introduced around `ExpectedS

- [`Program.cs`](Program.cs)
- [`Scenarios/GovernanceVersionedResolutionScenario.cs`](Scenarios/GovernanceVersionedResolutionScenario.cs)
- [`src/Governance/Runtime/Resolution/MutationRequestVersionResolver.cs`](../../../src/Governance/Runtime/Resolution/MutationRequestVersionResolver.cs)
- [`src/Governance/Runtime/Resolution/MutationRequestVersionResolutionManager.cs`](../../../src/Governance/Runtime/Resolution/MutationRequestVersionResolutionManager.cs)
- [`src/Governance/Abstractions/Resolution/VersionedRequestResolutionStrategy.cs`](../../../src/Governance/Abstractions/Resolution/VersionedRequestResolutionStrategy.cs)
- [`src/Governance/Abstractions/Resolution/MutationRequestVersionResolution.cs`](../../../src/Governance/Abstractions/Resolution/MutationRequestVersionResolution.cs)
- [`src/Governance/Runtime/Resolution/Execution/MutationRequestVersionResolver.cs`](../../../src/Governance/Runtime/Resolution/Execution/MutationRequestVersionResolver.cs)
- [`src/Governance/Runtime/Resolution/Execution/MutationRequestVersionResolutionManager.cs`](../../../src/Governance/Runtime/Resolution/Execution/MutationRequestVersionResolutionManager.cs)
- [`src/Governance/Abstractions/Resolution/Strategies/VersionedRequestResolutionStrategy.cs`](../../../src/Governance/Abstractions/Resolution/Strategies/VersionedRequestResolutionStrategy.cs)
- [`src/Governance/Abstractions/Resolution/Model/MutationRequestVersionResolution.cs`](../../../src/Governance/Abstractions/Resolution/Model/MutationRequestVersionResolution.cs)

## Run

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
using ModularityKit.Mutator.Abstractions.Context;
using ModularityKit.Mutator.Abstractions.Intent;
using ModularityKit.Mutator.Governance.Abstractions.Requests;
using ModularityKit.Mutator.Governance.Abstractions.Resolution;
using ModularityKit.Mutator.Governance.Runtime.Resolution;
using ModularityKit.Mutator.Governance.Abstractions.Requests.Model;
using ModularityKit.Mutator.Governance.Abstractions.Resolution.Model;
using ModularityKit.Mutator.Governance.Abstractions.Resolution.Strategies;
using ModularityKit.Mutator.Governance.Runtime.Resolution.Execution;
using ModularityKit.Mutator.Governance.Runtime.Storage;

namespace VersionedResolution.Scenarios;
Expand Down
Loading
Loading