refactor: replace fmt.Errorf/errors.New with coreerr.E()
Replace all remaining fmt.Errorf and errors.New calls in production
code with coreerr.E("caller.Method", "message", err) from go-log.
This standardises error handling across 23 files using the structured
error convention already established in the plugin package.
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
c2c54f1abb
commit
e9fc6902b1
23 changed files with 136 additions and 133 deletions
|
|
@ -2,10 +2,10 @@
|
|||
package agentci
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"forge.lthn.ai/core/config"
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
)
|
||||
|
||||
// AgentConfig represents a single agent machine in the config file.
|
||||
|
|
@ -43,7 +43,7 @@ func LoadAgents(cfg *config.Config) (map[string]AgentConfig, error) {
|
|||
continue
|
||||
}
|
||||
if ac.Host == "" {
|
||||
return nil, fmt.Errorf("agent %q: host is required", name)
|
||||
return nil, coreerr.E("agentci.LoadAgents", "agent "+name+": host is required", nil)
|
||||
}
|
||||
if ac.QueueDir == "" {
|
||||
ac.QueueDir = "/home/claude/ai-work/queue"
|
||||
|
|
@ -126,10 +126,10 @@ func SaveAgent(cfg *config.Config, name string, ac AgentConfig) error {
|
|||
func RemoveAgent(cfg *config.Config, name string) error {
|
||||
var agents map[string]AgentConfig
|
||||
if err := cfg.Get("agentci.agents", &agents); err != nil {
|
||||
return errors.New("no agents configured")
|
||||
return coreerr.E("agentci.RemoveAgent", "no agents configured", nil)
|
||||
}
|
||||
if _, ok := agents[name]; !ok {
|
||||
return fmt.Errorf("agent %q not found", name)
|
||||
return coreerr.E("agentci.RemoveAgent", "agent not found: "+name, nil)
|
||||
}
|
||||
delete(agents, name)
|
||||
return cfg.Set("agentci.agents", agents)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
package agentci
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
)
|
||||
|
||||
var safeNameRegex = regexp.MustCompile(`^[a-zA-Z0-9\-\_\.]+$`)
|
||||
|
|
@ -15,10 +16,10 @@ var safeNameRegex = regexp.MustCompile(`^[a-zA-Z0-9\-\_\.]+$`)
|
|||
func SanitizePath(input string) (string, error) {
|
||||
base := filepath.Base(input)
|
||||
if !safeNameRegex.MatchString(base) {
|
||||
return "", fmt.Errorf("invalid characters in path element: %s", input)
|
||||
return "", coreerr.E("agentci.SanitizePath", "invalid characters in path element: "+input, nil)
|
||||
}
|
||||
if base == "." || base == ".." || base == "/" {
|
||||
return "", fmt.Errorf("invalid path element: %s", base)
|
||||
return "", coreerr.E("agentci.SanitizePath", "invalid path element: "+base, nil)
|
||||
}
|
||||
return base, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
forgejo "codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2"
|
||||
|
||||
"forge.lthn.ai/core/cli/pkg/cli"
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
fg "forge.lthn.ai/core/go-scm/forge"
|
||||
)
|
||||
|
||||
|
|
@ -64,7 +65,7 @@ func runSync(args []string) error {
|
|||
if strings.HasPrefix(basePath, "~/") {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to resolve home directory: %w", err)
|
||||
return coreerr.E("forge.runSync", "failed to resolve home directory", err)
|
||||
}
|
||||
basePath = filepath.Join(home, basePath[2:])
|
||||
}
|
||||
|
|
@ -287,7 +288,7 @@ func syncConfigureForgeRemote(localPath, remoteURL string) error {
|
|||
if existing != remoteURL {
|
||||
cmd := exec.Command("git", "-C", localPath, "remote", "set-url", "forge", remoteURL)
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to update remote: %w", err)
|
||||
return coreerr.E("forge.syncConfigureForgeRemote", "failed to update remote", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
|
@ -295,7 +296,7 @@ func syncConfigureForgeRemote(localPath, remoteURL string) error {
|
|||
|
||||
cmd := exec.Command("git", "-C", localPath, "remote", "add", "forge", remoteURL)
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to add remote: %w", err)
|
||||
return coreerr.E("forge.syncConfigureForgeRemote", "failed to add remote", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -306,7 +307,7 @@ func syncPushUpstream(localPath, defaultBranch string) error {
|
|||
cmd := exec.Command("git", "-C", localPath, "push", "--force", "forge", refspec)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", strings.TrimSpace(string(output)), err)
|
||||
return coreerr.E("forge.syncPushUpstream", strings.TrimSpace(string(output)), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -316,7 +317,7 @@ func syncGitFetch(localPath, remote string) error {
|
|||
cmd := exec.Command("git", "-C", localPath, "fetch", remote)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", strings.TrimSpace(string(output)), err)
|
||||
return coreerr.E("forge.syncGitFetch", strings.TrimSpace(string(output)), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -327,7 +328,7 @@ func syncCreateMainFromUpstream(client *fg.Client, org, repo string) error {
|
|||
OldBranchName: "upstream",
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("create branch: %w", err)
|
||||
return coreerr.E("forge.syncCreateMainFromUpstream", "create branch", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"code.gitea.io/sdk/gitea"
|
||||
|
||||
"forge.lthn.ai/core/cli/pkg/cli"
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
gt "forge.lthn.ai/core/go-scm/gitea"
|
||||
)
|
||||
|
||||
|
|
@ -64,7 +65,7 @@ func runSync(args []string) error {
|
|||
if strings.HasPrefix(basePath, "~/") {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to resolve home directory: %w", err)
|
||||
return coreerr.E("gitea.runSync", "failed to resolve home directory", err)
|
||||
}
|
||||
basePath = filepath.Join(home, basePath[2:])
|
||||
}
|
||||
|
|
@ -299,7 +300,7 @@ func configureGiteaRemote(localPath, remoteURL string) error {
|
|||
if existing != remoteURL {
|
||||
cmd := exec.Command("git", "-C", localPath, "remote", "set-url", "gitea", remoteURL)
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to update remote: %w", err)
|
||||
return coreerr.E("gitea.configureGiteaRemote", "failed to update remote", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
|
@ -308,7 +309,7 @@ func configureGiteaRemote(localPath, remoteURL string) error {
|
|||
// Add new remote
|
||||
cmd := exec.Command("git", "-C", localPath, "remote", "add", "gitea", remoteURL)
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to add remote: %w", err)
|
||||
return coreerr.E("gitea.configureGiteaRemote", "failed to add remote", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -321,7 +322,7 @@ func pushUpstream(localPath, defaultBranch string) error {
|
|||
cmd := exec.Command("git", "-C", localPath, "push", "--force", "gitea", refspec)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", strings.TrimSpace(string(output)), err)
|
||||
return coreerr.E("gitea.pushUpstream", strings.TrimSpace(string(output)), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -332,7 +333,7 @@ func gitFetch(localPath, remote string) error {
|
|||
cmd := exec.Command("git", "-C", localPath, "fetch", remote)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", strings.TrimSpace(string(output)), err)
|
||||
return coreerr.E("gitea.gitFetch", strings.TrimSpace(string(output)), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -344,7 +345,7 @@ func createMainFromUpstream(client *gt.Client, org, repo string) error {
|
|||
OldBranchName: "upstream",
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("create branch: %w", err)
|
||||
return coreerr.E("gitea.createMainFromUpstream", "create branch", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package forge
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
forgejo "codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2"
|
||||
|
|
@ -75,7 +74,7 @@ func (c *Client) GetLabelByName(owner, repo, name string) (*forgejo.Label, error
|
|||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("forge.GetLabelByName: label %s not found in %s/%s", name, owner, repo)
|
||||
return nil, log.E("forge.GetLabelByName", "label "+name+" not found in "+owner+"/"+repo, nil)
|
||||
}
|
||||
|
||||
// EnsureLabel checks if a label exists, and creates it if it doesn't.
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"fmt"
|
||||
"time"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
"forge.lthn.ai/core/go-scm/forge"
|
||||
"forge.lthn.ai/core/go-scm/jobrunner"
|
||||
)
|
||||
|
|
@ -47,11 +48,11 @@ func (h *CompletionHandler) Execute(ctx context.Context, signal *jobrunner.Pipel
|
|||
if signal.Success {
|
||||
completeLabel, err := h.forge.EnsureLabel(signal.RepoOwner, signal.RepoName, LabelAgentComplete, ColorAgentComplete)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ensure label %s: %w", LabelAgentComplete, err)
|
||||
return nil, coreerr.E("completion.Execute", "ensure label "+LabelAgentComplete, err)
|
||||
}
|
||||
|
||||
if err := h.forge.AddIssueLabels(signal.RepoOwner, signal.RepoName, int64(signal.ChildNumber), []int64{completeLabel.ID}); err != nil {
|
||||
return nil, fmt.Errorf("add completed label: %w", err)
|
||||
return nil, coreerr.E("completion.Execute", "add completed label", err)
|
||||
}
|
||||
|
||||
if signal.Message != "" {
|
||||
|
|
@ -60,11 +61,11 @@ func (h *CompletionHandler) Execute(ctx context.Context, signal *jobrunner.Pipel
|
|||
} else {
|
||||
failedLabel, err := h.forge.EnsureLabel(signal.RepoOwner, signal.RepoName, LabelAgentFailed, ColorAgentFailed)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ensure label %s: %w", LabelAgentFailed, err)
|
||||
return nil, coreerr.E("completion.Execute", "ensure label "+LabelAgentFailed, err)
|
||||
}
|
||||
|
||||
if err := h.forge.AddIssueLabels(signal.RepoOwner, signal.RepoName, int64(signal.ChildNumber), []int64{failedLabel.ID}); err != nil {
|
||||
return nil, fmt.Errorf("add failed label: %w", err)
|
||||
return nil, coreerr.E("completion.Execute", "add failed label", err)
|
||||
}
|
||||
|
||||
msg := "Agent reported failure."
|
||||
|
|
|
|||
|
|
@ -8,10 +8,10 @@ import (
|
|||
"path/filepath"
|
||||
"time"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
"forge.lthn.ai/core/go-scm/agentci"
|
||||
"forge.lthn.ai/core/go-scm/forge"
|
||||
"forge.lthn.ai/core/go-scm/jobrunner"
|
||||
"forge.lthn.ai/core/go-log"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -83,23 +83,23 @@ func (h *DispatchHandler) Execute(ctx context.Context, signal *jobrunner.Pipelin
|
|||
|
||||
agentName, agent, ok := h.spinner.FindByForgejoUser(signal.Assignee)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown agent: %s", signal.Assignee)
|
||||
return nil, coreerr.E("dispatch.Execute", "unknown agent: "+signal.Assignee, nil)
|
||||
}
|
||||
|
||||
// Sanitize inputs to prevent path traversal.
|
||||
safeOwner, err := agentci.SanitizePath(signal.RepoOwner)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid repo owner: %w", err)
|
||||
return nil, coreerr.E("dispatch.Execute", "invalid repo owner", err)
|
||||
}
|
||||
safeRepo, err := agentci.SanitizePath(signal.RepoName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid repo name: %w", err)
|
||||
return nil, coreerr.E("dispatch.Execute", "invalid repo name", err)
|
||||
}
|
||||
|
||||
// Ensure in-progress label exists on repo.
|
||||
inProgressLabel, err := h.forge.EnsureLabel(safeOwner, safeRepo, LabelInProgress, ColorInProgress)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ensure label %s: %w", LabelInProgress, err)
|
||||
return nil, coreerr.E("dispatch.Execute", "ensure label "+LabelInProgress, err)
|
||||
}
|
||||
|
||||
// Check if already in progress to prevent double-dispatch.
|
||||
|
|
@ -107,7 +107,7 @@ func (h *DispatchHandler) Execute(ctx context.Context, signal *jobrunner.Pipelin
|
|||
if err == nil {
|
||||
for _, l := range issue.Labels {
|
||||
if l.Name == LabelInProgress || l.Name == LabelAgentComplete {
|
||||
log.Info("issue already processed, skipping", "issue", signal.ChildNumber, "label", l.Name)
|
||||
coreerr.Info("issue already processed, skipping", "issue", signal.ChildNumber, "label", l.Name)
|
||||
return &jobrunner.ActionResult{
|
||||
Action: "dispatch",
|
||||
Success: true,
|
||||
|
|
@ -120,11 +120,11 @@ func (h *DispatchHandler) Execute(ctx context.Context, signal *jobrunner.Pipelin
|
|||
|
||||
// Assign agent and add in-progress label.
|
||||
if err := h.forge.AssignIssue(safeOwner, safeRepo, int64(signal.ChildNumber), []string{signal.Assignee}); err != nil {
|
||||
log.Warn("failed to assign agent, continuing", "err", err)
|
||||
coreerr.Warn("failed to assign agent, continuing", "err", err)
|
||||
}
|
||||
|
||||
if err := h.forge.AddIssueLabels(safeOwner, safeRepo, int64(signal.ChildNumber), []int64{inProgressLabel.ID}); err != nil {
|
||||
return nil, fmt.Errorf("add in-progress label: %w", err)
|
||||
return nil, coreerr.E("dispatch.Execute", "add in-progress label", err)
|
||||
}
|
||||
|
||||
// Remove agent-ready label if present.
|
||||
|
|
@ -164,13 +164,13 @@ func (h *DispatchHandler) Execute(ctx context.Context, signal *jobrunner.Pipelin
|
|||
ticketJSON, err := json.MarshalIndent(ticket, "", " ")
|
||||
if err != nil {
|
||||
h.failDispatch(signal, "Failed to marshal ticket JSON")
|
||||
return nil, fmt.Errorf("marshal ticket: %w", err)
|
||||
return nil, coreerr.E("dispatch.Execute", "marshal ticket", err)
|
||||
}
|
||||
|
||||
// Check if ticket already exists on agent (dedup).
|
||||
ticketName := fmt.Sprintf("ticket-%s-%s-%d.json", safeOwner, safeRepo, signal.ChildNumber)
|
||||
if h.ticketExists(ctx, agent, ticketName) {
|
||||
log.Info("ticket already queued, skipping", "ticket", ticketName, "agent", signal.Assignee)
|
||||
coreerr.Info("ticket already queued, skipping", "ticket", ticketName, "agent", signal.Assignee)
|
||||
return &jobrunner.ActionResult{
|
||||
Action: "dispatch",
|
||||
RepoOwner: safeOwner,
|
||||
|
|
@ -263,7 +263,7 @@ func (h *DispatchHandler) secureTransfer(ctx context.Context, agent agentci.Agen
|
|||
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return log.E("dispatch.transfer", fmt.Sprintf("ssh to %s failed: %s", agent.Host, string(output)), err)
|
||||
return coreerr.E("dispatch.transfer", fmt.Sprintf("ssh to %s failed: %s", agent.Host, string(output)), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
|
||||
forgejosdk "codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
"forge.lthn.ai/core/go-scm/forge"
|
||||
"forge.lthn.ai/core/go-scm/jobrunner"
|
||||
)
|
||||
|
|
@ -39,7 +40,7 @@ func (h *DismissReviewsHandler) Execute(ctx context.Context, signal *jobrunner.P
|
|||
|
||||
reviews, err := h.forge.ListPRReviews(signal.RepoOwner, signal.RepoName, int64(signal.PRNumber))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dismiss_reviews: list reviews: %w", err)
|
||||
return nil, coreerr.E("dismiss_reviews.Execute", "list reviews", err)
|
||||
}
|
||||
|
||||
var dismissErrors []string
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
|
||||
forgejosdk "codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
"forge.lthn.ai/core/go-scm/forge"
|
||||
"forge.lthn.ai/core/go-scm/jobrunner"
|
||||
)
|
||||
|
|
@ -41,7 +42,7 @@ func (h *TickParentHandler) Execute(ctx context.Context, signal *jobrunner.Pipel
|
|||
// Fetch the epic issue body.
|
||||
epic, err := h.forge.GetIssue(signal.RepoOwner, signal.RepoName, int64(signal.EpicNumber))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("tick_parent: fetch epic: %w", err)
|
||||
return nil, coreerr.E("tick_parent.Execute", "fetch epic", err)
|
||||
}
|
||||
|
||||
oldBody := epic.Body
|
||||
|
|
|
|||
|
|
@ -2,14 +2,13 @@ package jobrunner
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
coreio "forge.lthn.ai/core/go-io"
|
||||
)
|
||||
|
||||
|
|
@ -55,7 +54,7 @@ type Journal struct {
|
|||
// NewJournal creates a new Journal rooted at baseDir.
|
||||
func NewJournal(baseDir string) (*Journal, error) {
|
||||
if baseDir == "" {
|
||||
return nil, errors.New("journal base directory is required")
|
||||
return nil, coreerr.E("jobrunner.NewJournal", "base directory is required", nil)
|
||||
}
|
||||
return &Journal{baseDir: baseDir}, nil
|
||||
}
|
||||
|
|
@ -66,12 +65,12 @@ func NewJournal(baseDir string) (*Journal, error) {
|
|||
func sanitizePathComponent(name string) (string, error) {
|
||||
// Reject empty or whitespace-only values.
|
||||
if name == "" || strings.TrimSpace(name) == "" {
|
||||
return "", fmt.Errorf("invalid path component: %q", name)
|
||||
return "", coreerr.E("jobrunner.sanitizePathComponent", "invalid path component: "+name, nil)
|
||||
}
|
||||
|
||||
// Reject inputs containing path separators (directory traversal attempt).
|
||||
if strings.ContainsAny(name, `/\`) {
|
||||
return "", fmt.Errorf("path component contains directory separator: %q", name)
|
||||
return "", coreerr.E("jobrunner.sanitizePathComponent", "path component contains directory separator: "+name, nil)
|
||||
}
|
||||
|
||||
// Use filepath.Clean to normalize (e.g., collapse redundant dots).
|
||||
|
|
@ -79,12 +78,12 @@ func sanitizePathComponent(name string) (string, error) {
|
|||
|
||||
// Reject traversal components.
|
||||
if clean == "." || clean == ".." {
|
||||
return "", fmt.Errorf("invalid path component: %q", name)
|
||||
return "", coreerr.E("jobrunner.sanitizePathComponent", "invalid path component: "+name, nil)
|
||||
}
|
||||
|
||||
// Validate against the safe character set.
|
||||
if !validPathComponent.MatchString(clean) {
|
||||
return "", fmt.Errorf("path component contains invalid characters: %q", name)
|
||||
return "", coreerr.E("jobrunner.sanitizePathComponent", "path component contains invalid characters: "+name, nil)
|
||||
}
|
||||
|
||||
return clean, nil
|
||||
|
|
@ -93,10 +92,10 @@ func sanitizePathComponent(name string) (string, error) {
|
|||
// Append writes a journal entry for the given signal and result.
|
||||
func (j *Journal) Append(signal *PipelineSignal, result *ActionResult) error {
|
||||
if signal == nil {
|
||||
return errors.New("signal is required")
|
||||
return coreerr.E("jobrunner.Journal.Append", "signal is required", nil)
|
||||
}
|
||||
if result == nil {
|
||||
return errors.New("result is required")
|
||||
return coreerr.E("jobrunner.Journal.Append", "result is required", nil)
|
||||
}
|
||||
|
||||
entry := JournalEntry{
|
||||
|
|
@ -124,18 +123,18 @@ func (j *Journal) Append(signal *PipelineSignal, result *ActionResult) error {
|
|||
|
||||
data, err := json.Marshal(entry)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal journal entry: %w", err)
|
||||
return coreerr.E("jobrunner.Journal.Append", "marshal journal entry", err)
|
||||
}
|
||||
data = append(data, '\n')
|
||||
|
||||
// Sanitize path components to prevent path traversal (CVE: issue #46).
|
||||
owner, err := sanitizePathComponent(signal.RepoOwner)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid repo owner: %w", err)
|
||||
return coreerr.E("jobrunner.Journal.Append", "invalid repo owner", err)
|
||||
}
|
||||
repo, err := sanitizePathComponent(signal.RepoName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid repo name: %w", err)
|
||||
return coreerr.E("jobrunner.Journal.Append", "invalid repo name", err)
|
||||
}
|
||||
|
||||
date := result.Timestamp.UTC().Format("2006-01-02")
|
||||
|
|
@ -144,27 +143,27 @@ func (j *Journal) Append(signal *PipelineSignal, result *ActionResult) error {
|
|||
// Resolve to absolute path and verify it stays within baseDir.
|
||||
absBase, err := filepath.Abs(j.baseDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolve base directory: %w", err)
|
||||
return coreerr.E("jobrunner.Journal.Append", "resolve base directory", err)
|
||||
}
|
||||
absDir, err := filepath.Abs(dir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolve journal directory: %w", err)
|
||||
return coreerr.E("jobrunner.Journal.Append", "resolve journal directory", err)
|
||||
}
|
||||
if !strings.HasPrefix(absDir, absBase+string(filepath.Separator)) {
|
||||
return fmt.Errorf("journal path %q escapes base directory %q", absDir, absBase)
|
||||
return coreerr.E("jobrunner.Journal.Append", "journal path escapes base directory", nil)
|
||||
}
|
||||
|
||||
j.mu.Lock()
|
||||
defer j.mu.Unlock()
|
||||
|
||||
if err := coreio.Local.EnsureDir(dir); err != nil {
|
||||
return fmt.Errorf("create journal directory: %w", err)
|
||||
return coreerr.E("jobrunner.Journal.Append", "create journal directory", err)
|
||||
}
|
||||
|
||||
path := filepath.Join(dir, date+".jsonl")
|
||||
f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open journal file: %w", err)
|
||||
return coreerr.E("jobrunner.Journal.Append", "open journal file", err)
|
||||
}
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@ package manifest
|
|||
import (
|
||||
"crypto/ed25519"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
"forge.lthn.ai/core/go-io"
|
||||
)
|
||||
|
||||
|
|
@ -35,19 +35,19 @@ type CompileOptions struct {
|
|||
// options. If opts.SignKey is provided the manifest is signed first.
|
||||
func Compile(m *Manifest, opts CompileOptions) (*CompiledManifest, error) {
|
||||
if m == nil {
|
||||
return nil, fmt.Errorf("manifest.Compile: nil manifest")
|
||||
return nil, coreerr.E("manifest.Compile", "nil manifest", nil)
|
||||
}
|
||||
if m.Code == "" {
|
||||
return nil, fmt.Errorf("manifest.Compile: missing code")
|
||||
return nil, coreerr.E("manifest.Compile", "missing code", nil)
|
||||
}
|
||||
if m.Version == "" {
|
||||
return nil, fmt.Errorf("manifest.Compile: missing version")
|
||||
return nil, coreerr.E("manifest.Compile", "missing version", nil)
|
||||
}
|
||||
|
||||
// Sign if a key is supplied.
|
||||
if opts.SignKey != nil {
|
||||
if err := Sign(m, opts.SignKey); err != nil {
|
||||
return nil, fmt.Errorf("manifest.Compile: %w", err)
|
||||
return nil, coreerr.E("manifest.Compile", "sign failed", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -69,7 +69,7 @@ func MarshalJSON(cm *CompiledManifest) ([]byte, error) {
|
|||
func ParseCompiled(data []byte) (*CompiledManifest, error) {
|
||||
var cm CompiledManifest
|
||||
if err := json.Unmarshal(data, &cm); err != nil {
|
||||
return nil, fmt.Errorf("manifest.ParseCompiled: %w", err)
|
||||
return nil, coreerr.E("manifest.ParseCompiled", "unmarshal failed", err)
|
||||
}
|
||||
return &cm, nil
|
||||
}
|
||||
|
|
@ -81,7 +81,7 @@ const compiledPath = "core.json"
|
|||
func WriteCompiled(medium io.Medium, root string, cm *CompiledManifest) error {
|
||||
data, err := MarshalJSON(cm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("manifest.WriteCompiled: %w", err)
|
||||
return coreerr.E("manifest.WriteCompiled", "marshal failed", err)
|
||||
}
|
||||
path := filepath.Join(root, compiledPath)
|
||||
return medium.Write(path, string(data))
|
||||
|
|
@ -92,7 +92,7 @@ func LoadCompiled(medium io.Medium, root string) (*CompiledManifest, error) {
|
|||
path := filepath.Join(root, compiledPath)
|
||||
data, err := medium.Read(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("manifest.LoadCompiled: %w", err)
|
||||
return nil, coreerr.E("manifest.LoadCompiled", "read failed", err)
|
||||
}
|
||||
return ParseCompiled([]byte(data))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ package manifest
|
|||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
"forge.lthn.ai/core/go-io"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
|
@ -21,7 +21,7 @@ func Load(medium io.Medium, root string) (*Manifest, error) {
|
|||
path := filepath.Join(root, manifestPath)
|
||||
data, err := medium.Read(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("manifest.Load: %w", err)
|
||||
return nil, coreerr.E("manifest.Load", "read failed", err)
|
||||
}
|
||||
return Parse([]byte(data))
|
||||
}
|
||||
|
|
@ -34,10 +34,10 @@ func LoadVerified(medium io.Medium, root string, pub ed25519.PublicKey) (*Manife
|
|||
}
|
||||
ok, err := Verify(m, pub)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("manifest.LoadVerified: %w", err)
|
||||
return nil, coreerr.E("manifest.LoadVerified", "verification error", err)
|
||||
}
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("manifest.LoadVerified: signature verification failed for %q", m.Code)
|
||||
return nil, coreerr.E("manifest.LoadVerified", "signature verification failed for "+m.Code, nil)
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
package manifest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
|
|
@ -66,7 +65,7 @@ type DaemonSpec struct {
|
|||
func Parse(data []byte) (*Manifest, error) {
|
||||
var m Manifest
|
||||
if err := yaml.Unmarshal(data, &m); err != nil {
|
||||
return nil, fmt.Errorf("manifest.Parse: %w", err)
|
||||
return nil, coreerr.E("manifest.Parse", "unmarshal failed", err)
|
||||
}
|
||||
return &m, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,8 @@ package manifest
|
|||
import (
|
||||
"crypto/ed25519"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
|
|
@ -20,7 +19,7 @@ func signable(m *Manifest) ([]byte, error) {
|
|||
func Sign(m *Manifest, priv ed25519.PrivateKey) error {
|
||||
msg, err := signable(m)
|
||||
if err != nil {
|
||||
return fmt.Errorf("manifest.Sign: marshal: %w", err)
|
||||
return coreerr.E("manifest.Sign", "marshal failed", err)
|
||||
}
|
||||
sig := ed25519.Sign(priv, msg)
|
||||
m.Sign = base64.StdEncoding.EncodeToString(sig)
|
||||
|
|
@ -30,15 +29,15 @@ func Sign(m *Manifest, priv ed25519.PrivateKey) error {
|
|||
// Verify checks the ed25519 signature in m.Sign against the public key.
|
||||
func Verify(m *Manifest, pub ed25519.PublicKey) (bool, error) {
|
||||
if m.Sign == "" {
|
||||
return false, errors.New("manifest.Verify: no signature present")
|
||||
return false, coreerr.E("manifest.Verify", "no signature present", nil)
|
||||
}
|
||||
sig, err := base64.StdEncoding.DecodeString(m.Sign)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("manifest.Verify: decode: %w", err)
|
||||
return false, coreerr.E("manifest.Verify", "decode failed", err)
|
||||
}
|
||||
msg, err := signable(m)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("manifest.Verify: marshal: %w", err)
|
||||
return false, coreerr.E("manifest.Verify", "marshal failed", err)
|
||||
}
|
||||
return ed25519.Verify(pub, msg, sig), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@ package marketplace
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
coreio "forge.lthn.ai/core/go-io"
|
||||
"forge.lthn.ai/core/go-scm/manifest"
|
||||
)
|
||||
|
|
@ -40,7 +40,7 @@ func (b *Builder) BuildFromDirs(dirs ...string) (*Index, error) {
|
|||
if os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
return nil, fmt.Errorf("marketplace.Builder: read %s: %w", dir, err)
|
||||
return nil, coreerr.E("marketplace.Builder.BuildFromDirs", "read "+dir, err)
|
||||
}
|
||||
|
||||
for _, e := range entries {
|
||||
|
|
@ -115,11 +115,11 @@ func BuildFromManifests(manifests []*manifest.Manifest) *Index {
|
|||
// WriteIndex serialises an Index to JSON and writes it to the given path.
|
||||
func WriteIndex(path string, idx *Index) error {
|
||||
if err := coreio.Local.EnsureDir(filepath.Dir(path)); err != nil {
|
||||
return fmt.Errorf("marketplace.WriteIndex: mkdir: %w", err)
|
||||
return coreerr.E("marketplace.WriteIndex", "mkdir failed", err)
|
||||
}
|
||||
data, err := json.MarshalIndent(idx, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("marketplace.WriteIndex: marshal: %w", err)
|
||||
return coreerr.E("marketplace.WriteIndex", "marshal failed", err)
|
||||
}
|
||||
return coreio.Local.Write(path, string(data))
|
||||
}
|
||||
|
|
@ -131,7 +131,7 @@ func (b *Builder) loadFromDir(dir string) (*manifest.Manifest, error) {
|
|||
if raw, err := coreio.Local.Read(coreJSON); err == nil {
|
||||
cm, err := manifest.ParseCompiled([]byte(raw))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse core.json: %w", err)
|
||||
return nil, coreerr.E("marketplace.Builder.loadFromDir", "parse core.json", err)
|
||||
}
|
||||
return &cm.Manifest, nil
|
||||
}
|
||||
|
|
@ -145,7 +145,7 @@ func (b *Builder) loadFromDir(dir string) (*manifest.Manifest, error) {
|
|||
|
||||
m, err := manifest.Parse([]byte(raw))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse manifest.yaml: %w", err)
|
||||
return nil, coreerr.E("marketplace.Builder.loadFromDir", "parse manifest.yaml", err)
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
package marketplace
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
coreio "forge.lthn.ai/core/go-io"
|
||||
"forge.lthn.ai/core/go-scm/manifest"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
|
@ -30,7 +30,7 @@ func DiscoverProviders(dir string) ([]DiscoveredProvider, error) {
|
|||
if os.IsNotExist(err) {
|
||||
return nil, nil // No providers directory — not an error.
|
||||
}
|
||||
return nil, fmt.Errorf("marketplace.DiscoverProviders: %w", err)
|
||||
return nil, coreerr.E("marketplace.DiscoverProviders", "read directory", err)
|
||||
}
|
||||
|
||||
var providers []DiscoveredProvider
|
||||
|
|
@ -93,12 +93,12 @@ func LoadProviderRegistry(path string) (*ProviderRegistryFile, error) {
|
|||
Providers: make(map[string]ProviderRegistryEntry),
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("marketplace.LoadProviderRegistry: %w", err)
|
||||
return nil, coreerr.E("marketplace.LoadProviderRegistry", "read failed", err)
|
||||
}
|
||||
|
||||
var reg ProviderRegistryFile
|
||||
if err := yaml.Unmarshal([]byte(raw), ®); err != nil {
|
||||
return nil, fmt.Errorf("marketplace.LoadProviderRegistry: %w", err)
|
||||
return nil, coreerr.E("marketplace.LoadProviderRegistry", "parse failed", err)
|
||||
}
|
||||
|
||||
if reg.Providers == nil {
|
||||
|
|
@ -111,12 +111,12 @@ func LoadProviderRegistry(path string) (*ProviderRegistryFile, error) {
|
|||
// SaveProviderRegistry writes the registry to the given path.
|
||||
func SaveProviderRegistry(path string, reg *ProviderRegistryFile) error {
|
||||
if err := coreio.Local.EnsureDir(filepath.Dir(path)); err != nil {
|
||||
return fmt.Errorf("marketplace.SaveProviderRegistry: %w", err)
|
||||
return coreerr.E("marketplace.SaveProviderRegistry", "ensure directory", err)
|
||||
}
|
||||
|
||||
data, err := yaml.Marshal(reg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marketplace.SaveProviderRegistry: %w", err)
|
||||
return coreerr.E("marketplace.SaveProviderRegistry", "marshal failed", err)
|
||||
}
|
||||
|
||||
return coreio.Local.Write(path, string(data))
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@ import (
|
|||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
"forge.lthn.ai/core/go-io"
|
||||
"forge.lthn.ai/core/go-scm/manifest"
|
||||
"forge.lthn.ai/core/go-io/store"
|
||||
|
|
@ -49,15 +49,15 @@ type InstalledModule struct {
|
|||
func (i *Installer) Install(ctx context.Context, mod Module) error {
|
||||
// Check if already installed
|
||||
if _, err := i.store.Get(storeGroup, mod.Code); err == nil {
|
||||
return fmt.Errorf("marketplace: module %q already installed", mod.Code)
|
||||
return coreerr.E("marketplace.Installer.Install", "module already installed: "+mod.Code, nil)
|
||||
}
|
||||
|
||||
dest := filepath.Join(i.modulesDir, mod.Code)
|
||||
if err := i.medium.EnsureDir(i.modulesDir); err != nil {
|
||||
return fmt.Errorf("marketplace: mkdir: %w", err)
|
||||
return coreerr.E("marketplace.Installer.Install", "mkdir", err)
|
||||
}
|
||||
if err := gitClone(ctx, mod.Repo, dest); err != nil {
|
||||
return fmt.Errorf("marketplace: clone %s: %w", mod.Repo, err)
|
||||
return coreerr.E("marketplace.Installer.Install", "clone "+mod.Repo, err)
|
||||
}
|
||||
|
||||
// On any error after clone, clean up the directory
|
||||
|
|
@ -70,7 +70,7 @@ func (i *Installer) Install(ctx context.Context, mod Module) error {
|
|||
|
||||
medium, err := io.NewSandboxed(dest)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marketplace: medium: %w", err)
|
||||
return coreerr.E("marketplace.Installer.Install", "medium", err)
|
||||
}
|
||||
|
||||
m, err := loadManifest(medium, mod.SignKey)
|
||||
|
|
@ -92,11 +92,11 @@ func (i *Installer) Install(ctx context.Context, mod Module) error {
|
|||
|
||||
data, err := json.Marshal(installed)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marketplace: marshal: %w", err)
|
||||
return coreerr.E("marketplace.Installer.Install", "marshal", err)
|
||||
}
|
||||
|
||||
if err := i.store.Set(storeGroup, mod.Code, string(data)); err != nil {
|
||||
return fmt.Errorf("marketplace: store: %w", err)
|
||||
return coreerr.E("marketplace.Installer.Install", "store", err)
|
||||
}
|
||||
|
||||
cleanup = false
|
||||
|
|
@ -106,7 +106,7 @@ func (i *Installer) Install(ctx context.Context, mod Module) error {
|
|||
// Remove uninstalls a module by deleting its files and store entry.
|
||||
func (i *Installer) Remove(code string) error {
|
||||
if _, err := i.store.Get(storeGroup, code); err != nil {
|
||||
return fmt.Errorf("marketplace: module %q not installed", code)
|
||||
return coreerr.E("marketplace.Installer.Remove", "module not installed: "+code, nil)
|
||||
}
|
||||
|
||||
dest := filepath.Join(i.modulesDir, code)
|
||||
|
|
@ -119,29 +119,29 @@ func (i *Installer) Remove(code string) error {
|
|||
func (i *Installer) Update(ctx context.Context, code string) error {
|
||||
raw, err := i.store.Get(storeGroup, code)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marketplace: module %q not installed", code)
|
||||
return coreerr.E("marketplace.Installer.Update", "module not installed: "+code, nil)
|
||||
}
|
||||
|
||||
var installed InstalledModule
|
||||
if err := json.Unmarshal([]byte(raw), &installed); err != nil {
|
||||
return fmt.Errorf("marketplace: unmarshal: %w", err)
|
||||
return coreerr.E("marketplace.Installer.Update", "unmarshal", err)
|
||||
}
|
||||
|
||||
dest := filepath.Join(i.modulesDir, code)
|
||||
|
||||
cmd := exec.CommandContext(ctx, "git", "-C", dest, "pull", "--ff-only")
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("marketplace: pull: %s: %w", strings.TrimSpace(string(output)), err)
|
||||
return coreerr.E("marketplace.Installer.Update", "pull: "+strings.TrimSpace(string(output)), err)
|
||||
}
|
||||
|
||||
// Reload and re-verify manifest with the same key used at install time
|
||||
medium, mErr := io.NewSandboxed(dest)
|
||||
if mErr != nil {
|
||||
return fmt.Errorf("marketplace: medium: %w", mErr)
|
||||
return coreerr.E("marketplace.Installer.Update", "medium", mErr)
|
||||
}
|
||||
m, mErr := loadManifest(medium, installed.SignKey)
|
||||
if mErr != nil {
|
||||
return fmt.Errorf("marketplace: reload manifest: %w", mErr)
|
||||
return coreerr.E("marketplace.Installer.Update", "reload manifest", mErr)
|
||||
}
|
||||
|
||||
// Update stored metadata
|
||||
|
|
@ -151,7 +151,7 @@ func (i *Installer) Update(ctx context.Context, code string) error {
|
|||
|
||||
data, err := json.Marshal(installed)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marketplace: marshal: %w", err)
|
||||
return coreerr.E("marketplace.Installer.Update", "marshal", err)
|
||||
}
|
||||
|
||||
return i.store.Set(storeGroup, code, string(data))
|
||||
|
|
@ -161,7 +161,7 @@ func (i *Installer) Update(ctx context.Context, code string) error {
|
|||
func (i *Installer) Installed() ([]InstalledModule, error) {
|
||||
all, err := i.store.GetAll(storeGroup)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marketplace: list: %w", err)
|
||||
return nil, coreerr.E("marketplace.Installer.Installed", "list", err)
|
||||
}
|
||||
|
||||
var modules []InstalledModule
|
||||
|
|
@ -180,7 +180,7 @@ func loadManifest(medium io.Medium, signKey string) (*manifest.Manifest, error)
|
|||
if signKey != "" {
|
||||
pubBytes, err := hex.DecodeString(signKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marketplace: decode sign key: %w", err)
|
||||
return nil, coreerr.E("marketplace.loadManifest", "decode sign key", err)
|
||||
}
|
||||
return manifest.LoadVerified(medium, ".", pubBytes)
|
||||
}
|
||||
|
|
@ -191,7 +191,7 @@ func loadManifest(medium io.Medium, signKey string) (*manifest.Manifest, error)
|
|||
func gitClone(ctx context.Context, repo, dest string) error {
|
||||
cmd := exec.CommandContext(ctx, "git", "clone", "--depth=1", repo, dest)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("%s: %w", strings.TrimSpace(string(output)), err)
|
||||
return coreerr.E("marketplace.gitClone", strings.TrimSpace(string(output)), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@ package marketplace
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
)
|
||||
|
||||
// Module is a marketplace entry pointing to a module's Git repo.
|
||||
|
|
@ -26,7 +27,7 @@ type Index struct {
|
|||
func ParseIndex(data []byte) (*Index, error) {
|
||||
var idx Index
|
||||
if err := json.Unmarshal(data, &idx); err != nil {
|
||||
return nil, fmt.Errorf("marketplace.ParseIndex: %w", err)
|
||||
return nil, coreerr.E("marketplace.ParseIndex", "unmarshal failed", err)
|
||||
}
|
||||
return &idx, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ func (i *Installer) cloneRepo(ctx context.Context, org, repo, version, dest stri
|
|||
|
||||
cmd := exec.CommandContext(ctx, "gh", args...)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("%w: %s", err, strings.TrimSpace(string(output)))
|
||||
return coreerr.E("plugin.Installer.cloneRepo", strings.TrimSpace(string(output)), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
package repos
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
"forge.lthn.ai/core/go-io"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
|
@ -44,12 +44,12 @@ func LoadGitState(m io.Medium, root string) (*GitState, error) {
|
|||
|
||||
content, err := m.Read(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read git state: %w", err)
|
||||
return nil, coreerr.E("repos.LoadGitState", "failed to read git state", err)
|
||||
}
|
||||
|
||||
var gs GitState
|
||||
if err := yaml.Unmarshal([]byte(content), &gs); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse git state: %w", err)
|
||||
return nil, coreerr.E("repos.LoadGitState", "failed to parse git state", err)
|
||||
}
|
||||
|
||||
if gs.Repos == nil {
|
||||
|
|
@ -66,17 +66,17 @@ func LoadGitState(m io.Medium, root string) (*GitState, error) {
|
|||
func SaveGitState(m io.Medium, root string, gs *GitState) error {
|
||||
coreDir := filepath.Join(root, ".core")
|
||||
if err := m.EnsureDir(coreDir); err != nil {
|
||||
return fmt.Errorf("failed to create .core directory: %w", err)
|
||||
return coreerr.E("repos.SaveGitState", "failed to create .core directory", err)
|
||||
}
|
||||
|
||||
data, err := yaml.Marshal(gs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal git state: %w", err)
|
||||
return coreerr.E("repos.SaveGitState", "failed to marshal git state", err)
|
||||
}
|
||||
|
||||
path := filepath.Join(coreDir, "git.yaml")
|
||||
if err := m.Write(path, string(data)); err != nil {
|
||||
return fmt.Errorf("failed to write git state: %w", err)
|
||||
return coreerr.E("repos.SaveGitState", "failed to write git state", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
"forge.lthn.ai/core/go-io"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
|
@ -74,12 +75,12 @@ func LoadKBConfig(m io.Medium, root string) (*KBConfig, error) {
|
|||
|
||||
content, err := m.Read(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read kb config: %w", err)
|
||||
return nil, coreerr.E("repos.LoadKBConfig", "failed to read kb config", err)
|
||||
}
|
||||
|
||||
kb := DefaultKBConfig()
|
||||
if err := yaml.Unmarshal([]byte(content), kb); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse kb config: %w", err)
|
||||
return nil, coreerr.E("repos.LoadKBConfig", "failed to parse kb config", err)
|
||||
}
|
||||
|
||||
return kb, nil
|
||||
|
|
@ -89,17 +90,17 @@ func LoadKBConfig(m io.Medium, root string) (*KBConfig, error) {
|
|||
func SaveKBConfig(m io.Medium, root string, kb *KBConfig) error {
|
||||
coreDir := filepath.Join(root, ".core")
|
||||
if err := m.EnsureDir(coreDir); err != nil {
|
||||
return fmt.Errorf("failed to create .core directory: %w", err)
|
||||
return coreerr.E("repos.SaveKBConfig", "failed to create .core directory", err)
|
||||
}
|
||||
|
||||
data, err := yaml.Marshal(kb)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal kb config: %w", err)
|
||||
return coreerr.E("repos.SaveKBConfig", "failed to marshal kb config", err)
|
||||
}
|
||||
|
||||
path := filepath.Join(coreDir, "kb.yaml")
|
||||
if err := m.Write(path, string(data)); err != nil {
|
||||
return fmt.Errorf("failed to write kb config: %w", err)
|
||||
return coreerr.E("repos.SaveKBConfig", "failed to write kb config", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -4,12 +4,11 @@
|
|||
package repos
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
"forge.lthn.ai/core/go-io"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
|
@ -67,13 +66,13 @@ type Repo struct {
|
|||
func LoadRegistry(m io.Medium, path string) (*Registry, error) {
|
||||
content, err := m.Read(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read registry file: %w", err)
|
||||
return nil, coreerr.E("repos.LoadRegistry", "failed to read registry file", err)
|
||||
}
|
||||
data := []byte(content)
|
||||
|
||||
var reg Registry
|
||||
if err := yaml.Unmarshal(data, ®); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse registry file: %w", err)
|
||||
return nil, coreerr.E("repos.LoadRegistry", "failed to parse registry file", err)
|
||||
}
|
||||
|
||||
reg.medium = m
|
||||
|
|
@ -147,7 +146,7 @@ func FindRegistry(m io.Medium) (string, error) {
|
|||
}
|
||||
}
|
||||
|
||||
return "", errors.New("repos.yaml not found")
|
||||
return "", coreerr.E("repos.FindRegistry", "repos.yaml not found", nil)
|
||||
}
|
||||
|
||||
// ScanDirectory creates a Registry by scanning a directory for git repos.
|
||||
|
|
@ -156,7 +155,7 @@ func FindRegistry(m io.Medium) (string, error) {
|
|||
func ScanDirectory(m io.Medium, dir string) (*Registry, error) {
|
||||
entries, err := m.List(dir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read directory: %w", err)
|
||||
return nil, coreerr.E("repos.ScanDirectory", "failed to read directory", err)
|
||||
}
|
||||
|
||||
reg := &Registry{
|
||||
|
|
@ -282,12 +281,12 @@ func (r *Registry) TopologicalOrder() ([]*Repo, error) {
|
|||
return nil
|
||||
}
|
||||
if visiting[name] {
|
||||
return fmt.Errorf("circular dependency detected: %s", name)
|
||||
return coreerr.E("repos.Registry.TopologicalOrder", "circular dependency detected: "+name, nil)
|
||||
}
|
||||
|
||||
repo, ok := r.Repos[name]
|
||||
if !ok {
|
||||
return fmt.Errorf("unknown repo: %s", name)
|
||||
return coreerr.E("repos.Registry.TopologicalOrder", "unknown repo: "+name, nil)
|
||||
}
|
||||
|
||||
visiting[name] = true
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
package repos
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
"forge.lthn.ai/core/go-io"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
|
@ -63,12 +63,12 @@ func LoadWorkConfig(m io.Medium, root string) (*WorkConfig, error) {
|
|||
|
||||
content, err := m.Read(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read work config: %w", err)
|
||||
return nil, coreerr.E("repos.LoadWorkConfig", "failed to read work config", err)
|
||||
}
|
||||
|
||||
wc := DefaultWorkConfig()
|
||||
if err := yaml.Unmarshal([]byte(content), wc); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse work config: %w", err)
|
||||
return nil, coreerr.E("repos.LoadWorkConfig", "failed to parse work config", err)
|
||||
}
|
||||
|
||||
return wc, nil
|
||||
|
|
@ -78,17 +78,17 @@ func LoadWorkConfig(m io.Medium, root string) (*WorkConfig, error) {
|
|||
func SaveWorkConfig(m io.Medium, root string, wc *WorkConfig) error {
|
||||
coreDir := filepath.Join(root, ".core")
|
||||
if err := m.EnsureDir(coreDir); err != nil {
|
||||
return fmt.Errorf("failed to create .core directory: %w", err)
|
||||
return coreerr.E("repos.SaveWorkConfig", "failed to create .core directory", err)
|
||||
}
|
||||
|
||||
data, err := yaml.Marshal(wc)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal work config: %w", err)
|
||||
return coreerr.E("repos.SaveWorkConfig", "failed to marshal work config", err)
|
||||
}
|
||||
|
||||
path := filepath.Join(coreDir, "work.yaml")
|
||||
if err := m.Write(path, string(data)); err != nil {
|
||||
return fmt.Errorf("failed to write work config: %w", err)
|
||||
return coreerr.E("repos.SaveWorkConfig", "failed to write work config", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue