Skip to content

Commit f4853cc

Browse files
authored
Feat: Implement governance approval workflow (#15)
2 parents 71fc021 + 962f10c commit f4853cc

64 files changed

Lines changed: 1378 additions & 119 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/publish-attested.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ name: Publish Attested
22

33
on:
44
workflow_dispatch:
5+
inputs:
6+
version:
7+
description: Release version without the leading "v"
8+
required: false
9+
type: string
510

611
permissions:
712
contents: write
@@ -12,6 +17,8 @@ permissions:
1217
jobs:
1318
publish:
1419
uses: ./.github/workflows/publish-artifacts.yml
20+
with:
21+
package_version: ${{ inputs.version }}
1522

1623
release:
1724
name: Upload artifacts to draft release

.github/workflows/release-drafter.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ jobs:
3737
publish:
3838
needs: update-release-draft
3939
uses: ./.github/workflows/publish-artifacts.yml
40+
with:
41+
package_version: ${{ needs.update-release-draft.outputs.tag_name }}
4042

4143
upload-release-assets:
4244
name: Upload artifacts to release draft

Docs/Decision/Adr/ADR_025_Governance_Approval_Workflow.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
#adr_025
55

66
## Status
7-
Proposed
7+
Accepted
88

99
## Date
1010
2026-06-22
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net10.0</TargetFramework>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<ProjectReference Include="..\..\..\src\ModularityKit.Mutator.Governance.csproj" />
12+
</ItemGroup>
13+
14+
</Project>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
using ApprovalWorkflow.Scenarios;
2+
3+
await GovernanceApprovalWorkflowScenario.Run();
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Governance ApprovalWorkflow
2+
3+
This example shows the governance approval workflow built on top of `MutationRequest.PendingApproval(...)` and `MutationRequestApprovalWorkflowManager`.
4+
5+
It demonstrates:
6+
7+
- mapping `PolicyRequirement` into request-level approval requirements
8+
- multi-actor approvals in the same step
9+
- ordered approval steps
10+
- transition from `Pending` to `Approved` after the final approval
11+
12+
## Key files
13+
14+
- [`Program.cs`](Program.cs)
15+
- [`Scenarios/GovernanceApprovalWorkflowScenario.cs`](Scenarios/GovernanceApprovalWorkflowScenario.cs)
16+
- [`src/Governance/Runtime/Approval/MutationRequestApprovalWorkflowManager.cs`](../../../src/Governance/Runtime/Approval/MutationRequestApprovalWorkflowManager.cs)
17+
- [`src/Governance/Abstractions/Approval/MutationApprovalRequirement.cs`](../../../src/Governance/Abstractions/Approval/MutationApprovalRequirement.cs)
18+
19+
## Run
20+
21+
```bash
22+
dotnet run --project Examples/Governance/ApprovalWorkflow/ApprovalWorkflow.csproj
23+
```
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
using ModularityKit.Mutator.Abstractions.Context;
2+
using ModularityKit.Mutator.Abstractions.Intent;
3+
using ModularityKit.Mutator.Abstractions.Policies;
4+
using ModularityKit.Mutator.Governance.Abstractions.Requests.Model;
5+
using ModularityKit.Mutator.Governance.Runtime.Approval.Execution;
6+
using ModularityKit.Mutator.Governance.Runtime.Storage;
7+
8+
namespace ApprovalWorkflow.Scenarios;
9+
10+
internal static class GovernanceApprovalWorkflowScenario
11+
{
12+
public static async Task Run()
13+
{
14+
var store = new InMemoryMutationRequestStore();
15+
var manager = new MutationRequestApprovalWorkflowManager(store);
16+
17+
PrintSection("Submit Pending Approval Request");
18+
var request = await store.Create(CreateApprovalRequest());
19+
PrintRequest(request);
20+
21+
PrintSection("Approve Step 1");
22+
var aliceApproval = request.ApprovalRequirements.Single(requirement => requirement.ApproverId == "alice");
23+
var afterAlice = await manager.ApproveRequirement(
24+
request.RequestId,
25+
aliceApproval.ApprovalId,
26+
MutationContext.User("alice", "Alice", "Manager approved"));
27+
PrintRequest(afterAlice);
28+
29+
PrintSection("Approve Step 1 - Second Actor");
30+
var bobApproval = afterAlice.ApprovalRequirements.Single(requirement => requirement.ApproverId == "bob");
31+
var afterBob = await manager.ApproveRequirement(
32+
request.RequestId,
33+
bobApproval.ApprovalId,
34+
MutationContext.User("bob", "Bob", "Security approved"));
35+
PrintRequest(afterBob);
36+
37+
PrintSection("Approve Step 2");
38+
var carolApproval = afterBob.ApprovalRequirements.Single(requirement => requirement.ApproverId == "carol");
39+
var afterCarol = await manager.ApproveRequirement(
40+
request.RequestId,
41+
carolApproval.ApprovalId,
42+
MutationContext.User("carol", "Carol", "Finance approved"));
43+
PrintRequest(afterCarol);
44+
}
45+
46+
private static MutationRequest CreateApprovalRequest()
47+
{
48+
return MutationRequest.PendingApproval(
49+
stateId: "tenant-42:roles",
50+
stateType: "IamRoleState",
51+
mutationType: "GrantRoleMutation",
52+
intent: new MutationIntent
53+
{
54+
OperationName = "GrantRole",
55+
Category = "Security",
56+
Description = "Grant elevated role to tenant operator"
57+
},
58+
context: MutationContext.User("requester", "Requester", "Need elevated access for incident"),
59+
requirements:
60+
[
61+
PolicyRequirement.Approval("alice", "Manager approval"),
62+
new PolicyRequirement
63+
{
64+
Type = "Approval",
65+
Description = "Security review",
66+
Data = new
67+
{
68+
Approver = "bob",
69+
StepOrder = 1,
70+
Reason = "Security sign-off"
71+
}
72+
},
73+
new PolicyRequirement
74+
{
75+
Type = "Approval",
76+
Description = "Finance review",
77+
Data = new
78+
{
79+
Approver = "carol",
80+
StepOrder = 2,
81+
Reason = "Budget sign-off"
82+
}
83+
}
84+
],
85+
expectedStateVersion: "v10");
86+
}
87+
88+
private static void PrintSection(string title)
89+
{
90+
Console.WriteLine();
91+
Console.WriteLine($"=== {title} ===");
92+
}
93+
94+
private static void PrintRequest(MutationRequest request)
95+
{
96+
Console.WriteLine($"Request status: {request.Status}");
97+
Console.WriteLine($"Pending reason: {request.PendingReason?.ToString() ?? "-"}");
98+
Console.WriteLine($"Revision: {request.Revision}");
99+
Console.WriteLine("Approval requirements:");
100+
101+
foreach (var requirement in request.ApprovalRequirements.OrderBy(requirement => requirement.StepOrder).ThenBy(requirement => requirement.ApproverId))
102+
{
103+
Console.WriteLine(
104+
$" - Step {requirement.StepOrder}: {requirement.ApproverId} => {requirement.Status}");
105+
}
106+
107+
var lastDecision = request.Decisions[^1];
108+
Console.WriteLine($"Last decision: {lastDecision.Type} by {lastDecision.Context.ActorId ?? "system"}");
109+
Console.WriteLine($"Reason: {lastDecision.Reason ?? "-"}");
110+
}
111+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net10.0</TargetFramework>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<ProjectReference Include="..\..\..\src\ModularityKit.Mutator.Governance.csproj" />
12+
</ItemGroup>
13+
14+
</Project>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
using DecisionTaxonomy.Scenarios;
2+
3+
GovernanceDecisionTaxonomyScenario.Run();
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Governance DecisionTaxonomy
2+
3+
This example shows why governance request decisions are split into separate categories instead of being kept in one flat enum.
4+
5+
It demonstrates:
6+
7+
- lifecycle decisions such as `Submitted` and `Approved`
8+
- approval decisions such as `Requested`, `Granted`, and `Rejected`
9+
- version-resolution decisions such as `Validated` and `RejectedAsStale`
10+
- the shared `MutationRequestDecisionType` wrapper with:
11+
- `Category`
12+
- `Code`
13+
- `ToString()`
14+
15+
## Key files
16+
17+
- [`Program.cs`](Program.cs)
18+
- [`Scenarios/GovernanceDecisionTaxonomyScenario.cs`](Scenarios/GovernanceDecisionTaxonomyScenario.cs)
19+
- [`src/Governance/Abstractions/Requests/Decisions/MutationRequestDecisionType.cs`](../../../src/Governance/Abstractions/Requests/Decisions/MutationRequestDecisionType.cs)
20+
- [`src/Governance/Abstractions/Requests/Decisions/MutationRequestDecisionCategory.cs`](../../../src/Governance/Abstractions/Requests/Decisions/MutationRequestDecisionCategory.cs)
21+
- [`src/Governance/Abstractions/Requests/Decisions/MutationRequestLifecycleDecisionType.cs`](../../../src/Governance/Abstractions/Requests/Decisions/MutationRequestLifecycleDecisionType.cs)
22+
- [`src/Governance/Abstractions/Requests/Decisions/MutationRequestApprovalDecisionType.cs`](../../../src/Governance/Abstractions/Requests/Decisions/MutationRequestApprovalDecisionType.cs)
23+
- [`src/Governance/Abstractions/Requests/Decisions/MutationRequestVersionResolutionDecisionType.cs`](../../../src/Governance/Abstractions/Requests/Decisions/MutationRequestVersionResolutionDecisionType.cs)
24+
25+
## Run
26+
27+
```bash
28+
dotnet run --project Examples/Governance/DecisionTaxonomy/DecisionTaxonomy.csproj
29+
```

0 commit comments

Comments
 (0)