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
28 changes: 28 additions & 0 deletions Compendium.sln
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Compendium.Abstractions.Cac
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Compendium.Abstractions.Caching.Tests", "tests\Unit\Compendium.Abstractions.Caching.Tests\Compendium.Abstractions.Caching.Tests.csproj", "{262B8E91-B53C-49BC-833D-739DD4CFC2E1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Compendium.Abstractions.Git", "src\Abstractions\Compendium.Abstractions.Git\Compendium.Abstractions.Git.csproj", "{25E575EB-5677-4BEE-A92A-D8832C27F87A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Compendium.Abstractions.Git.Tests", "tests\Unit\Compendium.Abstractions.Git.Tests\Compendium.Abstractions.Git.Tests.csproj", "{C0C1EAC1-5C33-4451-8CD6-2521A8D13557}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Compendium.Adapters.GitHub", "src\Adapters\Compendium.Adapters.GitHub\Compendium.Adapters.GitHub.csproj", "{4D9C4249-DD0F-4F07-B9FA-A42DAF5092CE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Compendium.Adapters.GitHub.Tests", "tests\Unit\Compendium.Adapters.GitHub.Tests\Compendium.Adapters.GitHub.Tests.csproj", "{5FE1601C-93E1-4E92-9AC9-521AA26592CB}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -472,6 +480,22 @@ Global
{262B8E91-B53C-49BC-833D-739DD4CFC2E1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{262B8E91-B53C-49BC-833D-739DD4CFC2E1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{262B8E91-B53C-49BC-833D-739DD4CFC2E1}.Release|Any CPU.Build.0 = Release|Any CPU
{25E575EB-5677-4BEE-A92A-D8832C27F87A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{25E575EB-5677-4BEE-A92A-D8832C27F87A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{25E575EB-5677-4BEE-A92A-D8832C27F87A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{25E575EB-5677-4BEE-A92A-D8832C27F87A}.Release|Any CPU.Build.0 = Release|Any CPU
{C0C1EAC1-5C33-4451-8CD6-2521A8D13557}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C0C1EAC1-5C33-4451-8CD6-2521A8D13557}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C0C1EAC1-5C33-4451-8CD6-2521A8D13557}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C0C1EAC1-5C33-4451-8CD6-2521A8D13557}.Release|Any CPU.Build.0 = Release|Any CPU
{4D9C4249-DD0F-4F07-B9FA-A42DAF5092CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4D9C4249-DD0F-4F07-B9FA-A42DAF5092CE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4D9C4249-DD0F-4F07-B9FA-A42DAF5092CE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4D9C4249-DD0F-4F07-B9FA-A42DAF5092CE}.Release|Any CPU.Build.0 = Release|Any CPU
{5FE1601C-93E1-4E92-9AC9-521AA26592CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5FE1601C-93E1-4E92-9AC9-521AA26592CB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5FE1601C-93E1-4E92-9AC9-521AA26592CB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5FE1601C-93E1-4E92-9AC9-521AA26592CB}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{FE421F00-7FFD-4666-A961-F1FF325ECD34} = {E35C8F52-5000-4427-9589-AEB5987C1AC6}
Expand Down Expand Up @@ -557,5 +581,9 @@ Global
{585E082A-E775-426B-BEA5-D41D3AF53DE7} = {6E0B453A-55CF-4567-ADBD-50CFB84CE629}
{4A6496B8-5833-428F-92D7-3EB331F5CA32} = {DE85A2F8-C0BA-47FD-8E9A-7D782FD6D3E2}
{262B8E91-B53C-49BC-833D-739DD4CFC2E1} = {6E0B453A-55CF-4567-ADBD-50CFB84CE629}
{25E575EB-5677-4BEE-A92A-D8832C27F87A} = {DE85A2F8-C0BA-47FD-8E9A-7D782FD6D3E2}
{C0C1EAC1-5C33-4451-8CD6-2521A8D13557} = {6E0B453A-55CF-4567-ADBD-50CFB84CE629}
{4D9C4249-DD0F-4F07-B9FA-A42DAF5092CE} = {73261E87-8FCA-40B6-940B-E25CBDBE33FB}
{5FE1601C-93E1-4E92-9AC9-521AA26592CB} = {6E0B453A-55CF-4567-ADBD-50CFB84CE629}
EndGlobalSection
EndGlobal
5 changes: 5 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@
<PackageVersion Include="NBomber.Http" Version="6.0.0" />
<PackageVersion Include="NetArchTest.Rules" Version="1.3.2" />
<PackageVersion Include="NSubstitute" Version="5.1.0" />
<!-- Compendium.Adapters.GitHub: GitHub App auth (Octokit) + Actions-secret sealing (libsodium).
Versions aligned with Nexus.Platform.GitHub, the source of the migrated protocol mechanics. -->
<PackageVersion Include="Octokit" Version="14.0.0" />
<PackageVersion Include="Sodium.Core" Version="1.4.0" />
<PackageVersion Include="WireMock.Net" Version="1.6.9" />
<!-- Npgsql + StackExchange.Redis remain as transitive pins for in-tree consumers
(AspNetCore health checks, sample 02). Direct PG/Redis adapter code lives in the
per-adapter repos (compendium-adapter-postgresql, compendium-adapter-redis). -->
Expand Down
10 changes: 10 additions & 0 deletions docs/adr/0006-multi-repo-adapter-split.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,4 +133,14 @@ Adapters extracted from this monorepo (chronological):

All seven heavy adapters now live in their own repositories. The framework's `tests/Integration` no longer requires Docker (per [ADR-0007](0007-integration-test-split-and-inmemory-defaults.md)); per-adapter integration tests live alongside the adapter that owns them. The framework CI's line-coverage gate is now strict at ≥ 90 %.

### In-tree adapters staged for extraction

New adapters may incubate in-tree while their abstraction stabilizes, then follow the same extraction path once the abstraction ships a stable (non-preview) tag:

| Date added in-tree | Adapter | External SDK(s) | Scheduled extraction repo | Extraction trigger |
|---|---|---|---|---|
| 2026-07-24 | `Compendium.Adapters.GitHub` | Octokit, Sodium.Core | `compendium-adapter-github` | First stable (non-preview) tag of `Compendium.Abstractions.Git`. |

`Compendium.Adapters.GitHub` is unit-testable end-to-end (WireMock for the REST paths, NSubstitute for the Octokit paths), so unlike the integration-bound adapters above it does not depress the framework's unit-coverage gate and can incubate in-tree without a coverage exemption until the Git abstraction stabilizes.

The convenience meta-package `Compendium.Extensions.ExternalAdapters` (which previously re-exposed Zitadel + Listmonk + LemonSqueezy DI helpers) was removed as part of this transition — consumers wire each adapter directly through its own DI extension method (`AddStripeBilling`, `AddZitadelIdentity`, etc.).
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// -----------------------------------------------------------------------
// <copyright file="IGitAccessControlService.cs" company="Sassy Solutions">
// Copyright (c) 2026 Sassy Solutions. Licensed under the MIT License.
// See LICENSE in the project root for license information.
// </copyright>
// -----------------------------------------------------------------------

using Compendium.Abstractions.Git.Connections;
using Compendium.Abstractions.Git.Repositories;

namespace Compendium.Abstractions.Git.AccessControl;

/// <summary>
/// Teams and repository access management on a namespace. Only meaningful for
/// organization namespaces — adapters fail with <c>Git.CapabilityNotSupported</c>
/// on user accounts. Requires
/// <see cref="Capabilities.GitCapability.TeamsAndPermissions"/>.
/// </summary>
public interface IGitAccessControlService
{
/// <summary>
/// Creates the team when absent, updates it otherwise (idempotent).
/// </summary>
Task<Result<GitTeam>> EnsureTeamAsync(
GitConnection connection,
string @namespace,
EnsureGitTeam request,
CancellationToken cancellationToken = default);

/// <summary>
/// Adds a user to a team (or updates their role in it).
/// </summary>
Task<Result> AddTeamMemberAsync(
GitConnection connection,
string @namespace,
string teamSlug,
string username,
GitTeamRole role,
CancellationToken cancellationToken = default);

/// <summary>
/// Grants (or updates) a team's role on a repository.
/// </summary>
Task<Result> SetTeamRepositoryRoleAsync(
GitConnection connection,
string @namespace,
string teamSlug,
GitRepositoryRef repository,
GitRepositoryRole role,
CancellationToken cancellationToken = default);

/// <summary>
/// Grants (or updates) an individual user's role on a repository.
/// </summary>
Task<Result> SetUserRepositoryRoleAsync(
GitConnection connection,
GitRepositoryRef repository,
string username,
GitRepositoryRole role,
CancellationToken cancellationToken = default);

/// <summary>
/// Removes a user's direct access to a repository. Removing absent access
/// succeeds (idempotent).
/// </summary>
Task<Result> RemoveUserFromRepositoryAsync(
GitConnection connection,
GitRepositoryRef repository,
string username,
CancellationToken cancellationToken = default);
}

/// <summary>
/// Request to create or update a team.
/// </summary>
public sealed record EnsureGitTeam
{
/// <summary>Gets the team display name; the provider derives the slug from it.</summary>
public required string Name { get; init; }

/// <summary>Gets the optional team description.</summary>
public string? Description { get; init; }
}

/// <summary>
/// A team on a namespace.
/// </summary>
/// <param name="Slug">The provider-side team slug used in subsequent calls.</param>
/// <param name="Name">The team display name.</param>
public sealed record GitTeam(string Slug, string Name);

/// <summary>
/// A user's role inside a team.
/// </summary>
public enum GitTeamRole
{
/// <summary>Regular team member.</summary>
Member,

/// <summary>Team maintainer (can manage membership).</summary>
Maintainer,
}

/// <summary>
/// A neutral repository access role. Adapters map onto their native role set
/// and document lossy mappings in their CAPABILITIES.md.
/// </summary>
public enum GitRepositoryRole
{
/// <summary>Read-only access.</summary>
Read,

/// <summary>Read plus issue/PR triage.</summary>
Triage,

/// <summary>Read/write access.</summary>
Write,

/// <summary>Write plus repository settings short of admin.</summary>
Maintain,

/// <summary>Full administrative access.</summary>
Admin,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// -----------------------------------------------------------------------
// <copyright file="GitCapability.cs" company="Sassy Solutions">
// Copyright (c) 2026 Sassy Solutions. Licensed under the MIT License.
// See LICENSE in the project root for license information.
// </copyright>
// -----------------------------------------------------------------------

namespace Compendium.Abstractions.Git.Capabilities;

/// <summary>
/// The optional capabilities a git-server adapter may support. Adapters declare
/// their support level per capability in <see cref="GitServerCapabilities"/>;
/// operations relying on an unsupported capability fail with
/// <c>Git.CapabilityNotSupported</c> instead of throwing.
/// </summary>
public enum GitCapability
{
/// <summary>Create a repository from a template/scaffold repository.</summary>
RepositoryFromTemplate,

/// <summary>Read repository metadata, contents, commits, and branches.</summary>
RepositoryManagement,

/// <summary>Create tags and publish releases.</summary>
TagsAndReleases,

/// <summary>Set repository-scoped CI secrets (encrypted, write-only).</summary>
CiSecrets,

/// <summary>Set repository-scoped CI variables (plaintext configuration).</summary>
CiVariables,

/// <summary>Set namespace-scoped (organization/group) CI secrets and variables.</summary>
NamespaceSecrets,

/// <summary>Trigger a CI pipeline / workflow run.</summary>
PipelineTrigger,

/// <summary>Read CI pipeline run status and history.</summary>
PipelineStatus,

/// <summary>Create and manage deployment environments.</summary>
DeploymentEnvironments,

/// <summary>Set secrets scoped to a deployment environment.</summary>
EnvironmentSecrets,

/// <summary>Apply branch protection policies (rulesets, protected branches).</summary>
BranchPolicies,

/// <summary>Manage teams and repository access roles.</summary>
TeamsAndPermissions,

/// <summary>Create and manage outgoing webhook subscriptions on repositories or namespaces.</summary>
WebhookManagement,

/// <summary>Verify and parse inbound webhook deliveries into neutral events.</summary>
WebhookIngestion,

/// <summary>Create the namespace (organization/group) itself via API.</summary>
NamespaceProvisioning,

/// <summary>Authenticate as a platform app installed on the customer namespace (e.g. GitHub App).</summary>
AppInstallationAuth,

/// <summary>Authenticate with a durable service-account / bot token.</summary>
ServiceAccountAuth,

/// <summary>Authenticate with an OAuth user access token.</summary>
OAuthUserAuth,

/// <summary>Narrow a minted credential to specific repositories/permissions at mint time.</summary>
ScopedTokenMinting,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// -----------------------------------------------------------------------
// <copyright file="GitServerCapabilities.cs" company="Sassy Solutions">
// Copyright (c) 2026 Sassy Solutions. Licensed under the MIT License.
// See LICENSE in the project root for license information.
// </copyright>
// -----------------------------------------------------------------------

namespace Compendium.Abstractions.Git.Capabilities;

/// <summary>
/// The support level an adapter declares for a <see cref="GitCapability"/>.
/// </summary>
public enum GitCapabilityLevel
{
/// <summary>The capability is not available on this provider.</summary>
None,

/// <summary>The capability is available with limitations (documented in the adapter's CAPABILITIES.md).</summary>
Partial,

/// <summary>The capability is fully supported.</summary>
Full,
}

/// <summary>
/// An adapter's declared support for a single capability.
/// </summary>
/// <param name="Level">The support level.</param>
/// <param name="Limitation">
/// A short human-readable limitation note, required when <paramref name="Level"/>
/// is <see cref="GitCapabilityLevel.Partial"/> or <see cref="GitCapabilityLevel.None"/>
/// for a capability the provider conceptually has but the adapter cannot reach
/// (e.g. "github.com org creation requires an enterprise-owner user token").
/// </param>
public sealed record GitCapabilitySupport(GitCapabilityLevel Level, string? Limitation = null);

/// <summary>
/// The declarative capability matrix of a git-server adapter. Consumers use it
/// to drive UI affordances (hide or disable unsupported features with a reason)
/// and adapters use <see cref="EnsureSupported"/> to fail uniformly.
/// </summary>
public sealed record GitServerCapabilities
{
/// <summary>
/// Gets the provider identifier these capabilities describe (matches
/// <see cref="IGitServer.Provider"/>).
/// </summary>
public required string Provider { get; init; }

/// <summary>
/// Gets the declared support per capability. Capabilities absent from the
/// dictionary are treated as <see cref="GitCapabilityLevel.None"/>.
/// </summary>
public required IReadOnlyDictionary<GitCapability, GitCapabilitySupport> Entries { get; init; }

/// <summary>
/// Returns whether the capability is available at any level
/// (<see cref="GitCapabilityLevel.Partial"/> or <see cref="GitCapabilityLevel.Full"/>).
/// </summary>
public bool Supports(GitCapability capability) =>
Entries.TryGetValue(capability, out var support) && support.Level != GitCapabilityLevel.None;

/// <summary>
/// Returns success when the capability is available, otherwise the standard
/// <c>Git.CapabilityNotSupported</c> failure. Adapters call this at the top
/// of optional-capability methods so every provider fails identically.
/// </summary>
public Result EnsureSupported(GitCapability capability) =>
Supports(capability)
? Result.Success()
: Result.Failure(GitErrors.NotSupported(
Provider,
capability,
Entries.TryGetValue(capability, out var support) ? support.Limitation : null));
}
Loading
Loading