fixes
This commit is contained in:
parent
1d9987c3ca
commit
3b50333353
|
|
@ -71,7 +71,7 @@ public class WorkflowFilesController : ControllerBase
|
|||
if (string.IsNullOrWhiteSpace(login))
|
||||
return StatusCode(503, new { error = "Forgejo login not resolved for this tenant." });
|
||||
|
||||
var cloneDir = _sourceFactory.Forgejo.TenantCloneDir(login);
|
||||
var cloneDir = _sourceFactory.Forgejo.TenantCloneDir(TenantId, login);
|
||||
var fullName = ForgejoWorkflowRepoService.RepoFullNameForLogin(login);
|
||||
var exists = Directory.Exists(Path.Combine(cloneDir, ".git"));
|
||||
|
||||
|
|
@ -230,7 +230,7 @@ public class WorkflowFilesController : ControllerBase
|
|||
if (string.IsNullOrWhiteSpace(login))
|
||||
return StatusCode(503, new { error = "Forgejo login not resolved for this tenant." });
|
||||
|
||||
var cloneDir = _sourceFactory.Forgejo.TenantCloneDir(login);
|
||||
var cloneDir = _sourceFactory.Forgejo.TenantCloneDir(TenantId, login);
|
||||
if (!Directory.Exists(Path.Combine(cloneDir, ".git")))
|
||||
return Ok(new { branch = "", changed = Array.Empty<object>(), untracked = Array.Empty<object>() });
|
||||
|
||||
|
|
@ -276,7 +276,7 @@ public class WorkflowFilesController : ControllerBase
|
|||
: request!.Message;
|
||||
|
||||
var (success, output) = await _sourceFactory.Forgejo.CommitAndPushAsync(
|
||||
login, message, HttpContext.RequestAborted);
|
||||
TenantId, login, message, HttpContext.RequestAborted);
|
||||
|
||||
_logger.LogInformation("Commit/push for login {Login}: {Success} ({Output})",
|
||||
login, success, output?.Trim() ?? "");
|
||||
|
|
@ -295,7 +295,7 @@ public class WorkflowFilesController : ControllerBase
|
|||
if (string.IsNullOrWhiteSpace(login))
|
||||
return StatusCode(503, new { error = "Forgejo login not resolved for this tenant." });
|
||||
|
||||
var ok = await _sourceFactory.Forgejo.PullAsync(login, HttpContext.RequestAborted);
|
||||
var ok = await _sourceFactory.Forgejo.PullAsync(TenantId, login, HttpContext.RequestAborted);
|
||||
return Ok(new { success = ok });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,9 +32,15 @@ public sealed class ForgejoWorkflowRepoService
|
|||
_forgejoBase = (config["Forgejo:BaseUrl"] ?? "https://forgejo.wiz4chat.com").TrimEnd('/');
|
||||
_forgejoToken = config["Forgejo:AccessToken"]?.Trim();
|
||||
_forgejoAdminToken = config["Forgejo:AdminToken"]?.Trim();
|
||||
_forgejoOwner = config["Forgejo:WorkflowRepoOwner"]
|
||||
?? config["Forgejo:Owner"]
|
||||
?? string.Empty;
|
||||
// Prefer WorkflowRepoOwner, but treat an explicit empty string as
|
||||
// "not configured" and fall back to Forgejo:Owner. A plain `??` is not
|
||||
// enough: appsettings ships `"WorkflowRepoOwner": ""`, which is non-null
|
||||
// and would win over Forgejo:Owner, silently disabling Forgejo-backed
|
||||
// workflow mode (IsConfigured reads owner as empty).
|
||||
var workflowRepoOwner = config["Forgejo:WorkflowRepoOwner"]?.Trim();
|
||||
_forgejoOwner = !string.IsNullOrWhiteSpace(workflowRepoOwner)
|
||||
? workflowRepoOwner
|
||||
: config["Forgejo:Owner"]?.Trim() ?? string.Empty;
|
||||
_copiesRoot = Path.GetFullPath(
|
||||
config["WorkflowSource:CopiesRoot"]
|
||||
?? throw new InvalidOperationException(
|
||||
|
|
@ -45,6 +51,17 @@ public sealed class ForgejoWorkflowRepoService
|
|||
/// <summary>The Forgejo base URL (for building repo links).</summary>
|
||||
public string ForgejoBaseUrl => _forgejoBase;
|
||||
|
||||
/// <summary>
|
||||
/// The token used for git transport (clone/pull/push) against the per-user
|
||||
/// private workflow repos. The repos are created and administered with the
|
||||
/// ADMIN token, so that is the credential that actually works against them;
|
||||
/// the plain AccessToken has no read/write grant on a user's private repo and
|
||||
/// Forgejo reports it as "Repository not found". Fall back to the access
|
||||
/// token when no admin token is configured.
|
||||
/// </summary>
|
||||
private string GitAuthToken =>
|
||||
!string.IsNullOrWhiteSpace(_forgejoAdminToken) ? _forgejoAdminToken : _forgejoToken;
|
||||
|
||||
/// <summary>True when Forgejo admin provisioning is configured.</summary>
|
||||
public bool IsConfigured =>
|
||||
!string.IsNullOrWhiteSpace(_forgejoAdminToken) &&
|
||||
|
|
@ -65,6 +82,20 @@ public sealed class ForgejoWorkflowRepoService
|
|||
public static string RepoFullNameForLogin(string login) =>
|
||||
$"{login.ToLowerInvariant()}/workflows-{Sanitize(login.ToLowerInvariant())}";
|
||||
|
||||
/// <summary>
|
||||
/// The source-copies owner-repo slug for a user's workflow repo, matching the
|
||||
/// webapi SourceCodeCopyService layout (<c>{owner}_{repo}</c>, e.g.
|
||||
/// <c>test_workflows-test</c>). The workflow repo is <c>{login}/workflows-{login}</c>,
|
||||
/// so the slug is <c>{login}_workflows-{login}</c>. Sharing this exact directory
|
||||
/// with the webapi source-code explorer is what keeps workflow edits visible in
|
||||
/// the source-code view without a manual fetch/pull.
|
||||
/// </summary>
|
||||
public static string OwnerRepoSlug(string login)
|
||||
{
|
||||
var owner = login.ToLowerInvariant();
|
||||
return $"{Sanitize(owner)}_workflows-{Sanitize(owner)}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures the user's Forgejo repo exists. Creates it via the admin API
|
||||
/// if missing (idempotent). Returns the repo full name (owner/repo).
|
||||
|
|
@ -132,9 +163,9 @@ public sealed class ForgejoWorkflowRepoService
|
|||
/// doesn't exist on Forgejo yet, it is created. If the clone directory is
|
||||
/// missing, the repo is cloned. Returns the absolute path to the local clone.
|
||||
/// </summary>
|
||||
public async Task<string> EnsureCloneAsync(string login, CancellationToken ct = default)
|
||||
public async Task<string> EnsureCloneAsync(string tenantId, string login, CancellationToken ct = default)
|
||||
{
|
||||
var tenantDir = TenantCloneDir(login);
|
||||
var tenantDir = TenantCloneDir(tenantId, login);
|
||||
|
||||
if (Directory.Exists(Path.Combine(tenantDir, ".git")))
|
||||
{
|
||||
|
|
@ -158,10 +189,16 @@ public sealed class ForgejoWorkflowRepoService
|
|||
CreateNoWindow = true,
|
||||
};
|
||||
psi.ArgumentList.Add("clone");
|
||||
if (!string.IsNullOrWhiteSpace(_forgejoToken))
|
||||
// The per-user workflow repos are created and managed via the Forgejo
|
||||
// ADMIN token, and each is private (owned by the user's own account).
|
||||
// The plain AccessToken has no grant on those private repos (Forgejo
|
||||
// returns 404), so cloning with it makes git fail with "Repository not
|
||||
// found". Use the admin token (fallback to the access token) so the
|
||||
// clone — and the later pull/push — succeed against the private repo.
|
||||
if (!string.IsNullOrWhiteSpace(GitAuthToken))
|
||||
{
|
||||
psi.ArgumentList.Add("-c");
|
||||
psi.ArgumentList.Add($"http.extraHeader=Authorization: token {_forgejoToken}");
|
||||
psi.ArgumentList.Add($"http.extraHeader=Authorization: token {GitAuthToken}");
|
||||
}
|
||||
psi.ArgumentList.Add("--");
|
||||
psi.ArgumentList.Add(cloneUrl);
|
||||
|
|
@ -200,16 +237,16 @@ public sealed class ForgejoWorkflowRepoService
|
|||
/// <summary>
|
||||
/// Pulls the latest changes from the remote. Returns true on success.
|
||||
/// </summary>
|
||||
public async Task<bool> PullAsync(string login, CancellationToken ct = default)
|
||||
public async Task<bool> PullAsync(string tenantId, string login, CancellationToken ct = default)
|
||||
{
|
||||
var tenantDir = TenantCloneDir(login);
|
||||
var tenantDir = TenantCloneDir(tenantId, login);
|
||||
if (!Directory.Exists(Path.Combine(tenantDir, ".git")))
|
||||
{
|
||||
_logger.LogWarning("No local clone for login {Login}, cannot pull", login);
|
||||
return false;
|
||||
}
|
||||
|
||||
var token = !string.IsNullOrWhiteSpace(_forgejoToken) ? _forgejoToken : _forgejoAdminToken;
|
||||
var token = GitAuthToken;
|
||||
var output = !string.IsNullOrWhiteSpace(token)
|
||||
? await RunGitAsync(tenantDir, ct, "-c", $"http.extraHeader=Authorization: token {token}", "pull", "--ff-only")
|
||||
: await RunGitAsync(tenantDir, ct, "pull", "--ff-only");
|
||||
|
|
@ -234,9 +271,9 @@ public sealed class ForgejoWorkflowRepoService
|
|||
/// edits via the source code API to persist changes to Forgejo.
|
||||
/// </summary>
|
||||
public async Task<(bool Success, string Output)> CommitAndPushAsync(
|
||||
string tenantId, string message, CancellationToken ct = default)
|
||||
string tenantId, string login, string message, CancellationToken ct = default)
|
||||
{
|
||||
var tenantDir = TenantCloneDir(tenantId);
|
||||
var tenantDir = TenantCloneDir(tenantId, login);
|
||||
if (!Directory.Exists(Path.Combine(tenantDir, ".git")))
|
||||
return (false, "No local clone");
|
||||
|
||||
|
|
@ -245,7 +282,7 @@ public sealed class ForgejoWorkflowRepoService
|
|||
if (commitOutput == null || commitOutput.Contains("nothing to commit", StringComparison.OrdinalIgnoreCase))
|
||||
return (true, "Nothing to commit");
|
||||
|
||||
var token = !string.IsNullOrWhiteSpace(_forgejoToken) ? _forgejoToken : _forgejoAdminToken;
|
||||
var token = GitAuthToken;
|
||||
var pushOutput = !string.IsNullOrWhiteSpace(token)
|
||||
? await RunGitAsync(tenantDir, ct, "-c", $"http.extraHeader=Authorization: token {token}", "push")
|
||||
: await RunGitAsync(tenantDir, ct, "push");
|
||||
|
|
@ -257,9 +294,17 @@ public sealed class ForgejoWorkflowRepoService
|
|||
return (ok, ok ? pushOutput?.Trim() ?? "Pushed" : pushOutput?.Trim() ?? "Push failed");
|
||||
}
|
||||
|
||||
/// <summary>The absolute path to a tenant's local clone directory.</summary>
|
||||
public string TenantCloneDir(string tenantId)
|
||||
=> Path.GetFullPath(Path.Combine(_copiesRoot, Sanitize(tenantId)));
|
||||
/// <summary>
|
||||
/// The absolute path to a tenant's local clone directory — shared with the
|
||||
/// webapi source-code explorer. Layout:
|
||||
/// <c>{CopiesRoot}/{tenantId}/{ownerRepo}</c> where <c>ownerRepo</c> is
|
||||
/// <see cref="OwnerRepoSlug"/> (e.g. <c>source-copies/10/test_workflows-test</c>).
|
||||
/// Both the w4c-workflows-api engine and the webapi SourceCodeController operate
|
||||
/// on this one working copy, so workflow edits and the source-code view stay
|
||||
/// consistent without a manual git pull.
|
||||
/// </summary>
|
||||
public string TenantCloneDir(string tenantId, string login)
|
||||
=> Path.GetFullPath(Path.Combine(_copiesRoot, Sanitize(tenantId), OwnerRepoSlug(login)));
|
||||
|
||||
private async Task<string?> RunGitAsync(string workDir, CancellationToken ct, params string[] args)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ public sealed class WorkflowSourceFactory
|
|||
public string ResolveTenantSourceDir(string tenantId, string? forgejoLogin = null)
|
||||
{
|
||||
if (_forgejo != null && !string.IsNullOrWhiteSpace(forgejoLogin))
|
||||
return _forgejo.TenantCloneDir(forgejoLogin);
|
||||
return _forgejo.TenantCloneDir(tenantId, forgejoLogin);
|
||||
return TenantSourceDir(tenantId);
|
||||
}
|
||||
|
||||
|
|
@ -127,10 +127,10 @@ public sealed class WorkflowSourceFactory
|
|||
}
|
||||
|
||||
// Ensure the Forgejo repo exists and is cloned locally.
|
||||
var cloneDir = await _forgejo.EnsureCloneAsync(login, ct);
|
||||
var cloneDir = await _forgejo.EnsureCloneAsync(tenantId, login, ct);
|
||||
|
||||
// Pull latest changes before reading.
|
||||
await _forgejo.PullAsync(login, ct);
|
||||
await _forgejo.PullAsync(tenantId, login, ct);
|
||||
|
||||
var logger = _loggers.CreateLogger<PerTenantWorkflowSource>();
|
||||
return new PerTenantWorkflowSource(tenantId, cloneDir, logger);
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ public class TaskDispatcher
|
|||
_db.TaskRuns.Add(taskRun);
|
||||
await _db.SaveChangesAsync(ct);
|
||||
|
||||
var workingDir = ResolveWorkingDir(run.TenantId, workflowPath);
|
||||
var workingDir = await ResolveWorkingDirAsync(run.TenantId, workflowPath, ct);
|
||||
var fields = TaskRunMessage.ToFields(run, task, input, workingDir, attempt);
|
||||
await _jobs.EnqueueAsync(run.TenantId, fields, ct);
|
||||
|
||||
|
|
@ -82,7 +82,7 @@ public class TaskDispatcher
|
|||
public async Task DeadLetterTaskAsync(
|
||||
WorkflowRun run, WorkflowTask task, TaskRun taskRun, CancellationToken ct)
|
||||
{
|
||||
var workingDir = ResolveWorkingDir(run.TenantId, run.Workflow.Path);
|
||||
var workingDir = await ResolveWorkingDirAsync(run.TenantId, run.Workflow.Path, ct);
|
||||
var fields = TaskRunMessage.ToFields(
|
||||
run, task, taskRun.InputJson, workingDir, taskRun.Attempt);
|
||||
|
||||
|
|
@ -124,6 +124,21 @@ public class TaskDispatcher
|
|||
return ResolveExistingDir(workerRoot, dir);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Async variant used by the live dispatch path. In Forgejo-backed mode the
|
||||
/// working root is the tenant's shared local clone, whose path depends on the
|
||||
/// user's Forgejo login — resolved here (async, DB-backed) so the worker runs
|
||||
/// the exact files being edited in the source-code explorer.
|
||||
/// </summary>
|
||||
public async Task<string> ResolveWorkingDirAsync(string tenantId, string workflowPath, CancellationToken ct = default)
|
||||
{
|
||||
var workerRoot = await ResolveWorkerRootForTenantAsync(tenantId, ct);
|
||||
var dir = Path.GetDirectoryName(workflowPath) ?? string.Empty;
|
||||
if (string.IsNullOrWhiteSpace(dir))
|
||||
return workerRoot;
|
||||
return ResolveExistingDir(workerRoot, dir);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Legacy overload for backward compatibility. Resolves against the default worker root.
|
||||
/// </summary>
|
||||
|
|
@ -168,10 +183,20 @@ public class TaskDispatcher
|
|||
|
||||
private string ResolveWorkerRootForTenant(string tenantId)
|
||||
{
|
||||
// In Forgejo-backed mode, resolve from the tenant's local clone.
|
||||
// Filesystem-only mode: use the global worker root.
|
||||
return _defaultWorkerRoot;
|
||||
}
|
||||
|
||||
private async Task<string> ResolveWorkerRootForTenantAsync(string tenantId, CancellationToken ct)
|
||||
{
|
||||
// In Forgejo-backed mode, resolve from the tenant's shared local clone,
|
||||
// whose path depends on the user's Forgejo login (resolved via the factory).
|
||||
if (_sourceFactory?.IsForgejoBacked == true && _sourceFactory.Forgejo != null)
|
||||
{
|
||||
return _sourceFactory.Forgejo.TenantCloneDir(tenantId);
|
||||
var login = await _sourceFactory.ResolveForgejoLoginAsync(tenantId, ct);
|
||||
if (!string.IsNullOrEmpty(login))
|
||||
return _sourceFactory.Forgejo.TenantCloneDir(tenantId, login);
|
||||
return _defaultWorkerRoot;
|
||||
}
|
||||
|
||||
// Filesystem-only mode: use the global worker root.
|
||||
|
|
|
|||
|
|
@ -53,7 +53,9 @@ public class WorkflowSyncService
|
|||
if (_sourceFactory?.IsForgejoBacked == true && _sourceFactory.Forgejo != null)
|
||||
{
|
||||
_logger.LogDebug("Pulling latest workflow files from Forgejo for tenant {TenantId}", tenantId);
|
||||
await _sourceFactory.Forgejo.PullAsync(tenantId, ct);
|
||||
var login = await _sourceFactory.ResolveForgejoLoginAsync(tenantId, ct);
|
||||
if (!string.IsNullOrEmpty(login))
|
||||
await _sourceFactory.Forgejo.PullAsync(tenantId, login, ct);
|
||||
}
|
||||
|
||||
var state = _source.GetState();
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@
|
|||
"RepoRoot": ""
|
||||
},
|
||||
"WorkflowSource": {
|
||||
"CopiesRoot": ".data/workflow-tenants",
|
||||
"CopiesRoot": "../source-copies",
|
||||
"SharedDir": "workflows"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue