From 9f408b90aa0fe863a6aac18241bd7fb8c86e92cd Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Jul 2026 03:25:52 +0000 Subject: [PATCH 1/4] Add license signature trust-store extension seam MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gated (private desktop repo) needs multi-key trust-ring lookup and revocation for license verification, but under the Phase 2 submodule model it consumes Trackdub.Licensing read-only and can no longer fork LicenseTokenValidator/LicenseService in place to get it, per docs/plans/trackdub-gated-split-manifest.md (Trackdub-gated PR #2) §1. Adds ILicenseSignatureTrustStore: resolves a PEM public key per token key id, or null to reject an unknown/revoked key. LicenseService takes it as a new optional constructor parameter; omitting it preserves the existing single embedded-key behavior exactly. LicenseTokenClaims gains an optional KeyId parsed from the token's "kid" claim and threaded into LicenseTokenValidator.VerifySignature, which now resolves the key per-store when one is supplied instead of always using the embedded key. Revocation falls out of the same seam: a trust store returning null for a given key id fails verification closed, so no separate revocation plumbing is needed. Everything added is internal to Trackdub.Licensing except the new public interface and the optional constructor parameter, so the project keeps its zero-ProjectReference, BCL-only-crypto invariants (LicensingIsolationTests) and the dependency graph in AGENTS.md is unchanged. Production-policy concerns from the same manifest section -- rejecting dev-unlimited tokens under a production trust ring, and startup validation of production ring configuration -- are deliberately left out of this seam. Both read from already-public LicenseValidationResult fields (UnlimitedActivations, degradation reason) and are naturally implementable as a decorator over ILicenseInitializer/ILicenseTierProvider in the consuming product, without needing any further core change. The manifest's other blocker, headless export-tier enforcement, needs no core change at all: Trackdub.Sdk's TrackdubBuilder.ConfigureServices (-> TrackdubOptions/HeadlessTrackdubOptions.ServiceConfigurator -> HeadlessCompositionRoot.AddHeadlessTrackdub step 7) already lets a consumer register a custom IExportTierGate for Cli/Sdk hosts. Trackdub.Cli goes through this same builder. That seam already exists; using it is Phase 2.3 work in the desktop repo, not something this PR needs to add. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_014Yd883rGkFx5xN1TyT1Lzf --- .../ILicenseSignatureTrustStore.cs | 29 ++++ src/Trackdub.Licensing/LicenseService.cs | 15 +- src/Trackdub.Licensing/LicenseTokenClaims.cs | 3 +- src/Trackdub.Licensing/LicenseTokenParser.cs | 6 +- .../LicenseTokenValidator.cs | 40 ++++- .../LicenseSignatureTrustStoreTests.cs | 153 ++++++++++++++++++ .../TokenSignatureRoundTripTests.cs | 12 +- 7 files changed, 247 insertions(+), 11 deletions(-) create mode 100644 src/Trackdub.Licensing/ILicenseSignatureTrustStore.cs create mode 100644 tests/Trackdub.Licensing.Tests/LicenseSignatureTrustStoreTests.cs diff --git a/src/Trackdub.Licensing/ILicenseSignatureTrustStore.cs b/src/Trackdub.Licensing/ILicenseSignatureTrustStore.cs new file mode 100644 index 0000000..e42d890 --- /dev/null +++ b/src/Trackdub.Licensing/ILicenseSignatureTrustStore.cs @@ -0,0 +1,29 @@ +namespace Trackdub.Licensing; + +/// +/// Resolves the trusted public key used to verify a license token's ES256 signature. +/// +/// +/// The public core verifies tokens against a single embedded development key by +/// default (see the parameterless constructor). A +/// consuming product that needs multi-key rotation or revocation supplies its own +/// trust policy by implementing this interface and passing it to +/// itself never +/// implements or requires one. +/// +public interface ILicenseSignatureTrustStore +{ + /// + /// Resolves the PEM-encoded ECDSA P-256 public key trusted for the given key id, + /// or null to reject the token — e.g. because the key id is unknown, or the key + /// has been revoked. + /// + /// + /// The key id taken from the token's unverified claims. It is itself untrusted: + /// callers must not treat it as authenticated until the key it resolves to goes + /// on to successfully verify the token's signature. May be null for tokens that + /// carry no key id; implementations decide whether that resolves to a default + /// key or is rejected. + /// + string? ResolvePublicKeyPem(string? keyId); +} diff --git a/src/Trackdub.Licensing/LicenseService.cs b/src/Trackdub.Licensing/LicenseService.cs index 568ccb6..71cf9fa 100644 --- a/src/Trackdub.Licensing/LicenseService.cs +++ b/src/Trackdub.Licensing/LicenseService.cs @@ -17,13 +17,22 @@ public sealed class LicenseService : ILicenseInitializer, ILicenseTierProvider private LicenseValidationResult _validationResult; + /// + /// Optional signature trust policy. When omitted, tokens are verified against the + /// single embedded development public key, matching prior behavior. A consuming + /// product that needs multi-key rotation or revocation supplies its own + /// implementation here. + /// public LicenseService( IHardwareFingerprintProvider fingerprintProvider, - ILogger logger) + ILogger logger, + ILicenseSignatureTrustStore? trustStore = null) { _fileStore = new LicenseFileStore(); _parser = new LicenseTokenParser(); - _validator = new LicenseTokenValidator(); + _validator = trustStore is not null + ? new LicenseTokenValidator(trustStore) + : new LicenseTokenValidator(); _fingerprintProvider = fingerprintProvider; _logger = logger; _validationResult = new LicenseValidationResult(LicenseTier.Free, null, 0, 0, null, null); @@ -75,7 +84,7 @@ public Task InitializeAsync(CancellationToken cancellat // 3. Verify signature var sigParts = _parser.GetSignatureParts(token); - if (sigParts is null || !_validator.VerifySignature(sigParts.Value.SigningInput, sigParts.Value.Signature)) + if (sigParts is null || !_validator.VerifySignature(claims.KeyId, sigParts.Value.SigningInput, sigParts.Value.Signature)) { _logger.LogWarning("License token signature verification failed."); _validationResult = new LicenseValidationResult(LicenseTier.Free, claims.Sub, 0, 0, null, "Invalid signature"); diff --git a/src/Trackdub.Licensing/LicenseTokenClaims.cs b/src/Trackdub.Licensing/LicenseTokenClaims.cs index e764d4a..7c58475 100644 --- a/src/Trackdub.Licensing/LicenseTokenClaims.cs +++ b/src/Trackdub.Licensing/LicenseTokenClaims.cs @@ -9,4 +9,5 @@ internal sealed record LicenseTokenClaims( IReadOnlyList Machines, long Iat, long? Exp, - bool DevUnlimited = false); + bool DevUnlimited = false, + string? KeyId = null); diff --git a/src/Trackdub.Licensing/LicenseTokenParser.cs b/src/Trackdub.Licensing/LicenseTokenParser.cs index 6fa254b..e5ff02d 100644 --- a/src/Trackdub.Licensing/LicenseTokenParser.cs +++ b/src/Trackdub.Licensing/LicenseTokenParser.cs @@ -42,7 +42,8 @@ internal sealed class LicenseTokenParser Machines: payload.Machines ?? [], Iat: payload.Iat, Exp: payload.Exp, - DevUnlimited: payload.DevUnlimited ?? false); + DevUnlimited: payload.DevUnlimited ?? false, + KeyId: payload.KeyId); } catch (FormatException) { @@ -105,5 +106,8 @@ private sealed record PayloadDto [System.Text.Json.Serialization.JsonPropertyName("dev_unlimited")] public bool? DevUnlimited { get; init; } + + [System.Text.Json.Serialization.JsonPropertyName("kid")] + public string? KeyId { get; init; } } } diff --git a/src/Trackdub.Licensing/LicenseTokenValidator.cs b/src/Trackdub.Licensing/LicenseTokenValidator.cs index b8425d5..c57f423 100644 --- a/src/Trackdub.Licensing/LicenseTokenValidator.cs +++ b/src/Trackdub.Licensing/LicenseTokenValidator.cs @@ -18,7 +18,8 @@ internal sealed class LicenseTokenValidator -----END PUBLIC KEY----- """; - private readonly ECDsa _ecdsa; + private readonly ECDsa? _ecdsa; + private readonly ILicenseSignatureTrustStore? _trustStore; /// /// Creates a validator using the embedded production public key. @@ -38,16 +39,49 @@ internal LicenseTokenValidator(string publicKeyPem) _ecdsa.ImportFromPem(publicKeyPem); } + /// + /// Creates a validator that resolves the trusted key per-token via + /// instead of the embedded key. + /// + internal LicenseTokenValidator(ILicenseSignatureTrustStore trustStore) + { + _trustStore = trustStore; + } + /// /// Verifies the ES256 signature over the signing input bytes. /// Returns true if the signature is valid, false otherwise. /// Never throws. /// - public bool VerifySignature(byte[] signingInput, byte[] signature) + /// + /// The unverified key id from the token's claims. Ignored when this validator + /// was constructed without a trust store; the embedded key is used in that case. + /// + public bool VerifySignature(string? keyId, byte[] signingInput, byte[] signature) { + if (_trustStore is not null) + { + var publicKeyPem = _trustStore.ResolvePublicKeyPem(keyId); + if (publicKeyPem is null) + { + return false; + } + + try + { + using var ecdsa = ECDsa.Create(); + ecdsa.ImportFromPem(publicKeyPem); + return ecdsa.VerifyData(signingInput, signature, HashAlgorithmName.SHA256); + } + catch + { + return false; + } + } + try { - return _ecdsa.VerifyData(signingInput, signature, HashAlgorithmName.SHA256); + return _ecdsa!.VerifyData(signingInput, signature, HashAlgorithmName.SHA256); } catch { diff --git a/tests/Trackdub.Licensing.Tests/LicenseSignatureTrustStoreTests.cs b/tests/Trackdub.Licensing.Tests/LicenseSignatureTrustStoreTests.cs new file mode 100644 index 0000000..a382e35 --- /dev/null +++ b/tests/Trackdub.Licensing.Tests/LicenseSignatureTrustStoreTests.cs @@ -0,0 +1,153 @@ +using System.Security.Cryptography; +using Microsoft.Extensions.Logging.Abstractions; +using Xunit; + +namespace Trackdub.Licensing.Tests; + +// Covers the ILicenseSignatureTrustStore seam: a consuming product can supply its own +// multi-key trust policy without patching Trackdub.Licensing internals. +public sealed class LicenseSignatureTrustStoreTests +{ + [Fact] + public async Task Token_signed_with_trust_store_key_verifies_and_resolves_pro_tier() + { + var tempBase = Directory.CreateTempSubdirectory().FullName; + try + { + using var key = ECDsa.Create(ECCurve.NamedCurves.nistP256); + var publicKeyPem = key.ExportSubjectPublicKeyInfoPem(); + + var token = TestTokenBuilder.BuildSignedToken( + sub: "user-1", + tier: "pro", + machines: ["sha256:current"], + iat: 1_700_000_000, + exp: 2_000_000_000, + keyId: "ring-key-1", + signingKey: key); + + var store = new LicenseFileStore(tempBase); + store.WriteToken(token); + + var trustStore = new FakeTrustStore(("ring-key-1", publicKeyPem)); + var service = new LicenseService( + new StaticFingerprintProvider("sha256:current"), + NullLogger.Instance, + trustStore); + + var result = await service.InitializeAsync(); + + Assert.Equal(LicenseTier.Pro, result.Tier); + Assert.Null(result.DegradationReason); + var requestedKeyId = Assert.Single(trustStore.RequestedKeyIds); + Assert.Equal("ring-key-1", requestedKeyId); + } + finally + { + Directory.Delete(tempBase, true); + } + } + + [Fact] + public async Task Unknown_key_id_is_rejected_even_with_otherwise_valid_signature() + { + var tempBase = Directory.CreateTempSubdirectory().FullName; + try + { + using var key = ECDsa.Create(ECCurve.NamedCurves.nistP256); + + var token = TestTokenBuilder.BuildSignedToken( + sub: "user-1", + tier: "pro", + machines: ["sha256:current"], + iat: 1_700_000_000, + exp: 2_000_000_000, + keyId: "revoked-key", + signingKey: key); + + var store = new LicenseFileStore(tempBase); + store.WriteToken(token); + + // Trust store knows nothing about "revoked-key" -- simulates an unknown or + // revoked key id. ResolvePublicKeyPem returning null must fail verification + // closed, not fall back to any other key. + var trustStore = new FakeTrustStore(); + var service = new LicenseService( + new StaticFingerprintProvider("sha256:current"), + NullLogger.Instance, + trustStore); + + var result = await service.InitializeAsync(); + + Assert.Equal(LicenseTier.Free, result.Tier); + Assert.Equal("Invalid signature", result.DegradationReason); + } + finally + { + Directory.Delete(tempBase, true); + } + } + + [Fact] + public async Task Signature_from_wrong_key_fails_even_when_key_id_resolves() + { + var tempBase = Directory.CreateTempSubdirectory().FullName; + try + { + using var signingKey = ECDsa.Create(ECCurve.NamedCurves.nistP256); + using var differentKey = ECDsa.Create(ECCurve.NamedCurves.nistP256); + + var token = TestTokenBuilder.BuildSignedToken( + sub: "user-1", + tier: "pro", + machines: ["sha256:current"], + iat: 1_700_000_000, + exp: 2_000_000_000, + keyId: "ring-key-1", + signingKey: signingKey); + + var store = new LicenseFileStore(tempBase); + store.WriteToken(token); + + // Trust store resolves "ring-key-1" to a *different* public key than the one + // that actually signed the token. + var trustStore = new FakeTrustStore(("ring-key-1", differentKey.ExportSubjectPublicKeyInfoPem())); + var service = new LicenseService( + new StaticFingerprintProvider("sha256:current"), + NullLogger.Instance, + trustStore); + + var result = await service.InitializeAsync(); + + Assert.Equal(LicenseTier.Free, result.Tier); + Assert.Equal("Invalid signature", result.DegradationReason); + } + finally + { + Directory.Delete(tempBase, true); + } + } + + private sealed class FakeTrustStore : ILicenseSignatureTrustStore + { + private readonly Dictionary _keysByKeyId; + + public FakeTrustStore(params (string KeyId, string PublicKeyPem)[] keys) + { + _keysByKeyId = keys.ToDictionary(k => k.KeyId, k => k.PublicKeyPem); + } + + public List RequestedKeyIds { get; } = []; + + public string? ResolvePublicKeyPem(string? keyId) + { + RequestedKeyIds.Add(keyId); + return keyId is not null && _keysByKeyId.TryGetValue(keyId, out var pem) ? pem : null; + } + } + + private sealed class StaticFingerprintProvider(string fingerprint) : IHardwareFingerprintProvider + { + public string GetFingerprint() => fingerprint; + } +} diff --git a/tests/Trackdub.Licensing.Tests/TokenSignatureRoundTripTests.cs b/tests/Trackdub.Licensing.Tests/TokenSignatureRoundTripTests.cs index 5eafb12..1dc01b0 100644 --- a/tests/Trackdub.Licensing.Tests/TokenSignatureRoundTripTests.cs +++ b/tests/Trackdub.Licensing.Tests/TokenSignatureRoundTripTests.cs @@ -42,7 +42,7 @@ from exp in Gen.Frequency( return false.Label("Parse returned null"); // Assert: signature is valid - var signatureValid = validator.VerifySignature(sigParts.Value.SigningInput, sigParts.Value.Signature); + var signatureValid = validator.VerifySignature(claims.KeyId, sigParts.Value.SigningInput, sigParts.Value.Signature); if (!signatureValid) return false.Label("Signature validation failed"); @@ -88,7 +88,9 @@ public static string BuildSignedToken( IReadOnlyList machines, long iat, long? exp, - bool devUnlimited = false) + bool devUnlimited = false, + string? keyId = null, + ECDsa? signingKey = null) { var header = Base64UrlEncode(JsonSerializer.SerializeToUtf8Bytes(new { alg = "ES256", typ = "JWT" })); var payloadObj = new Dictionary @@ -103,10 +105,14 @@ public static string BuildSignedToken( { payloadObj["dev_unlimited"] = true; } + if (keyId is not null) + { + payloadObj["kid"] = keyId; + } var payload = Base64UrlEncode(JsonSerializer.SerializeToUtf8Bytes(payloadObj)); var signingInput = $"{header}.{payload}"; - var signatureBytes = Ecdsa.SignData(Encoding.UTF8.GetBytes(signingInput), HashAlgorithmName.SHA256); + var signatureBytes = (signingKey ?? Ecdsa).SignData(Encoding.UTF8.GetBytes(signingInput), HashAlgorithmName.SHA256); var signature = Base64UrlEncode(signatureBytes); return $"{header}.{payload}.{signature}"; From f0cf730114a0aa5c0c912c61e7144ea177b130b1 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Jul 2026 03:30:08 +0000 Subject: [PATCH 2/4] Fix trust-store tests to actually use the temp token store Found by review on PR #7. All three LicenseSignatureTrustStoreTests constructed LicenseService via its public constructor, which always builds its own LicenseFileStore() (the real platform app-data directory) regardless of the trustStore argument. The tempBase LicenseFileStore each test wrote the token to was never passed in, so in a clean environment the service would read no token, short-circuit to Free with a null degradation reason, and the assertions would fail without ever exercising the trust store. Switched all three to the internal 5-arg constructor already used by every other test in this file's project (GracefulDegradationTests, DevUnlimitedTokenTests, TierResolutionCorrectnessTests), passing the temp-backed store explicitly and wrapping the trust store in LicenseTokenValidator(trustStore) directly. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_014Yd883rGkFx5xN1TyT1Lzf --- .../LicenseSignatureTrustStoreTests.cs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/tests/Trackdub.Licensing.Tests/LicenseSignatureTrustStoreTests.cs b/tests/Trackdub.Licensing.Tests/LicenseSignatureTrustStoreTests.cs index a382e35..e75a2d5 100644 --- a/tests/Trackdub.Licensing.Tests/LicenseSignatureTrustStoreTests.cs +++ b/tests/Trackdub.Licensing.Tests/LicenseSignatureTrustStoreTests.cs @@ -31,9 +31,11 @@ public async Task Token_signed_with_trust_store_key_verifies_and_resolves_pro_ti var trustStore = new FakeTrustStore(("ring-key-1", publicKeyPem)); var service = new LicenseService( + store, + new LicenseTokenParser(), + new LicenseTokenValidator(trustStore), new StaticFingerprintProvider("sha256:current"), - NullLogger.Instance, - trustStore); + NullLogger.Instance); var result = await service.InitializeAsync(); @@ -73,9 +75,11 @@ public async Task Unknown_key_id_is_rejected_even_with_otherwise_valid_signature // closed, not fall back to any other key. var trustStore = new FakeTrustStore(); var service = new LicenseService( + store, + new LicenseTokenParser(), + new LicenseTokenValidator(trustStore), new StaticFingerprintProvider("sha256:current"), - NullLogger.Instance, - trustStore); + NullLogger.Instance); var result = await service.InitializeAsync(); @@ -113,9 +117,11 @@ public async Task Signature_from_wrong_key_fails_even_when_key_id_resolves() // that actually signed the token. var trustStore = new FakeTrustStore(("ring-key-1", differentKey.ExportSubjectPublicKeyInfoPem())); var service = new LicenseService( + store, + new LicenseTokenParser(), + new LicenseTokenValidator(trustStore), new StaticFingerprintProvider("sha256:current"), - NullLogger.Instance, - trustStore); + NullLogger.Instance); var result = await service.InitializeAsync(); From 6b054a54e8377556e7507cf748e49fb47c1cb1b7 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Jul 2026 03:34:14 +0000 Subject: [PATCH 3/4] Preserve binary compatibility of LicenseService's two-arg constructor Found by review on PR #7. The trust-store parameter was added as an optional third parameter on the existing two-arg constructor. Optional parameters are a compile-time/source feature only -- the CLR method signature still requires all parameters, so a precompiled consumer holding a reference to the original two-arg constructor would hit MissingMethodException if Trackdub.Licensing.dll were upgraded without recompiling that consumer. This project publishes NuGet packages (submit-nuget workflow), so that scenario is real. Splits into two real constructor overloads: the original two-arg signature is now a distinct compiled method that delegates to a non-optional three-arg overload carrying the trust store. Both existing source call sites (implicit two-arg, and any future explicit three-arg) keep compiling; the two-arg CLR signature itself no longer changes. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_014Yd883rGkFx5xN1TyT1Lzf --- src/Trackdub.Licensing/LicenseService.cs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/Trackdub.Licensing/LicenseService.cs b/src/Trackdub.Licensing/LicenseService.cs index 71cf9fa..a2854d1 100644 --- a/src/Trackdub.Licensing/LicenseService.cs +++ b/src/Trackdub.Licensing/LicenseService.cs @@ -17,16 +17,23 @@ public sealed class LicenseService : ILicenseInitializer, ILicenseTierProvider private LicenseValidationResult _validationResult; + public LicenseService( + IHardwareFingerprintProvider fingerprintProvider, + ILogger logger) + : this(fingerprintProvider, logger, trustStore: null) + { + } + /// - /// Optional signature trust policy. When omitted, tokens are verified against the - /// single embedded development public key, matching prior behavior. A consuming - /// product that needs multi-key rotation or revocation supplies its own - /// implementation here. + /// Signature trust policy. When null, tokens are verified against the single + /// embedded development public key, matching the two-argument constructor's + /// behavior. A consuming product that needs multi-key rotation or revocation + /// supplies its own implementation here. /// public LicenseService( IHardwareFingerprintProvider fingerprintProvider, ILogger logger, - ILicenseSignatureTrustStore? trustStore = null) + ILicenseSignatureTrustStore? trustStore) { _fileStore = new LicenseFileStore(); _parser = new LicenseTokenParser(); From 2ff45f73252692101d2752f84f519d8f3b69adb9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Jul 2026 03:48:24 +0000 Subject: [PATCH 4/4] Move CI off self-hosted runners onto GitHub-hosted Self-hosted runner availability was already flagged as a Phase 1 publish-readiness blocker and deprioritized rather than fixed; the queued-forever Build & Test / Verify Code Format / Verify Repository Boundary checks on this PR are that blocker showing up directly. Switches ci.yml (format, repository-boundary, build-windows, build-linux, build-macos), model-audit.yml (all three jobs), and code-coverage.yml to ubuntu-latest/windows-latest/macos-latest as appropriate, and dependabot-auto-merge.yml to ubuntu-latest. None of these have a hardware dependency -- standard dotnet build/test/format, Python scripts, or gh CLI. codeql.yml already targeted ubuntu-latest/windows-latest and needed no change. Deliberately left trt-rtx-smoke.yml on self-hosted. It validates the TensorRT-RTX GPU execution provider against real NVIDIA GPU hardware, which standard GitHub-hosted runners don't have; flipping its runs-on would silently turn it into a no-op (it already has continue-on-error: true and is gated behind an opt-in repo variable) rather than fix anything. That one needs an actual GPU runner, not a relabel. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_014Yd883rGkFx5xN1TyT1Lzf --- .github/workflows/ci.yml | 10 +++++----- .github/workflows/code-coverage.yml | 2 +- .github/workflows/dependabot-auto-merge.yml | 2 +- .github/workflows/model-audit.yml | 6 +++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 31f301e..7ff15c7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ permissions: jobs: format: name: Verify Code Format - runs-on: [self-hosted, Linux] + runs-on: ubuntu-latest timeout-minutes: 15 steps: - name: Checkout Source @@ -52,7 +52,7 @@ jobs: repository-boundary: name: Verify Repository Boundary - runs-on: [self-hosted, Linux] + runs-on: ubuntu-latest timeout-minutes: 5 steps: - name: Checkout Source @@ -65,7 +65,7 @@ jobs: build-windows: name: Build & Test (Windows) timeout-minutes: 45 - runs-on: [self-hosted, Windows] + runs-on: windows-latest steps: - name: Checkout Source uses: actions/checkout@v7 @@ -96,7 +96,7 @@ jobs: build-linux: name: Build & Test (Linux) timeout-minutes: 45 - runs-on: [self-hosted, Linux] + runs-on: ubuntu-latest steps: - name: Checkout Source uses: actions/checkout@v7 @@ -127,7 +127,7 @@ jobs: build-macos: name: Build & Test (macOS) timeout-minutes: 45 - runs-on: [self-hosted, macOS] + runs-on: macos-latest steps: - name: Checkout Source uses: actions/checkout@v7 diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index 5491054..4051554 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -18,7 +18,7 @@ permissions: jobs: dotnet: name: .NET coverage - runs-on: self-hosted + runs-on: ubuntu-latest timeout-minutes: 45 steps: - name: Checkout repository diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index 5a9e4d2..151a51c 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -11,7 +11,7 @@ permissions: jobs: dependabot: name: Auto-merge Dependabot PR - runs-on: self-hosted + runs-on: ubuntu-latest if: github.event.pull_request.user.login == 'dependabot[bot]' steps: - name: Dependabot metadata diff --git a/.github/workflows/model-audit.yml b/.github/workflows/model-audit.yml index 47b4e60..5c3f3d4 100644 --- a/.github/workflows/model-audit.yml +++ b/.github/workflows/model-audit.yml @@ -9,7 +9,7 @@ permissions: jobs: optimize_ci: name: Optimize CI - runs-on: self-hosted + runs-on: ubuntu-latest outputs: skip: ${{ steps.check_skip.outputs.skip }} steps: @@ -23,7 +23,7 @@ jobs: name: Validate model manifest needs: optimize_ci if: needs.optimize_ci.outputs.skip == 'false' - runs-on: self-hosted + runs-on: ubuntu-latest timeout-minutes: 10 steps: @@ -49,7 +49,7 @@ jobs: name: Verify HF manifest hashes needs: optimize_ci if: needs.optimize_ci.outputs.skip == 'false' && (github.event_name == 'workflow_dispatch' || github.event_name == 'schedule') - runs-on: self-hosted + runs-on: ubuntu-latest timeout-minutes: 180 steps: