-
Notifications
You must be signed in to change notification settings - Fork 14
feat: CP-955 migrate ProjectErrorsTool #3688
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
), | ||
Handler: func(ctx context.Context, p *primer.Values, mcpRequest mcp.CallToolRequest) (*mcp.CallToolResult, error) { | ||
namespace, _ := mcpRequest.RequireString("namespace") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't throw away errors.
ns, err := project.ParseNamespace(namespace) | ||
if err != nil { | ||
return mcp.NewToolResultError(fmt.Errorf("invalid namespace format. Use 'owner/project' format: %w", err).Error()), nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return mcp.NewToolResultError(fmt.Errorf("invalid namespace format. Use 'owner/project' format: %w", err).Error()), nil | |
return mcp.NewToolResultError(fmt.Sprintf("Could not parse namespace: %s", errs.JoinMessage(err))), nil |
- Don't make assumptions about what the underlying errors is, the error it returns should (and does) give the format error. But it doesn't necessarily have to be a format error.
- No point passing it through
Errorf()
if you're just going to grab the message at the end. - Always use
errs.JoinMessage(err)
or equivalent to get all underlying errors. You don't know if the direct error you're receiving has the actual cause of the issue in it.
err = runner.Run() | ||
if err != nil { | ||
return mcp.NewToolResultError(fmt.Errorf("error executing GraphQL query: %v", err).Error()), nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return mcp.NewToolResultError(fmt.Errorf("error executing GraphQL query: %v", err).Error()), nil | |
return mcp.NewToolResultError(fmt.Sprintf("error executing GraphQL query: %s", errs.JoinMessage(err))), nil |
) | ||
|
||
type ProjectErrorsRunner struct { | ||
primer *primer.Values |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please record the underlying values here. The primers use-case is passing large amount of context. This runner only needs some of that context, which it should specify with an interface.
See for example:
return &Hello{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added output, auth and svcModel, which are coming from the primer
Name: artifact.Name(), | ||
Version: artifact.Version(), | ||
Namespace: artifact.Ingredients[0].Namespace, | ||
LogURL: artifact.LogURL, | ||
WasFixed: artifact.Revision() < int(*ingredient.Revision), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is called FailedIngredient but other than the namespace is talking about an artifact. We should clearly differentiate between the two. Ingredient names and Artifact names are not always direct mappings.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I renamed it to FailedArtifact
ingredient, err := model.GetIngredientByNameAndVersion( | ||
artifact.Ingredients[0].Namespace, artifact.Name(), artifact.Version(), nil, runner.primer.Auth()) | ||
if err != nil { | ||
return fmt.Errorf("error searching ingredient: %w", err) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This might be slow if you have a lot of failing artifacts. Probably fine for a first iteration, but might make sense to use hasura here to query multiple ingredients at the same time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes I thought about that too. For now it doesn't seem to bad. I will add a note so we don't forget about it.
// Check if ingredients are failing due to missing dependencies. | ||
err = CheckDependencyErrors(&failedIngredients) | ||
if err != nil { | ||
return fmt.Errorf("error checking dependency errors: %w", err) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you should pass this failedArtifacts. You're not relying on WasFixed
which is the only piece of meta info that failedIngredients has that failedArtifacts doesn't.
That way you can call this earlier, before failedIngredients is set, and you're not waiting on remotely pulling in all relevant ingredients before you error out on an unrelated error.
I see this is adding context to the failedIngredients, but you could always have this return a map that you then use to associate it to ingredients further down.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
var wg sync.WaitGroup | ||
var mu sync.Mutex |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, now you're getting to the good stuff! ;)
return | ||
} | ||
ingredient.IsDependencyError = depError | ||
}(&(*failedIngredients)[i]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure I follow why this is creating a new pointer? Could conceivably just be
}(&(*failedIngredients)[i]) | |
}(failedIngredients[i]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I get "invalid operation: cannot index failedArtifacts (variable of type *[]FailedArtifact)"
failedArtifacts is a pointer, and this is why I can't index it. I need to dereference using *, but I can do that before "range artifacts" and use &artifacts[i] here
var MissingDependencyKeywords = map[string][]string{ | ||
"language/php": { | ||
"require_once(): Failed opening required", | ||
"include(): Failed opening", | ||
"require(): Failed opening required", | ||
"include_once(): Failed opening", | ||
"Fatal error: Uncaught Error: Class", | ||
"Class not found", | ||
}, | ||
"language/perl": { | ||
"Can't locate", | ||
"in @INC", | ||
"BEGIN failed--compilation aborted", | ||
}, | ||
"language/python": { | ||
"ModuleNotFoundError", | ||
"No module named", | ||
"ImportError", | ||
}, | ||
"language/tcl": { | ||
"can't find package", | ||
"couldn't load library", | ||
"package require", | ||
}, | ||
"language/ruby": { | ||
"LoadError", | ||
"cannot load such file", | ||
"no such file to load", | ||
"in `require'", | ||
}, | ||
"language/c-sharp": { | ||
"CS0246", | ||
"are you missing a using directive", | ||
"could not be found", | ||
"The type or namespace", | ||
}, | ||
"shared": { | ||
"unresolved import", "no external crate", "E0432", "E0463", "can't find crate", // RUST | ||
"No such file or directory", "fatal error:", "undefined reference to", // C/CPP | ||
}, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Many of these are far too vague to directly map to missing dependency errors.
I'd strongly prefer if this tool tried to find the most relevant parts of the log, rather than make any assertions about what the log is saying. The LLM will be MUCH better suited for this task than us keyword matching a string.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I rolled it back to check for ModuleNotFoundError as in its Python implementation.
For now, it's known to only work for Python. I added comments saying that we need to improve it to consider other languages and use a more robust mechanism
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for sticking with me, nice work!
type ArtifactOutput struct { | ||
Name string `json:"name"` | ||
Version string `json:"version"` | ||
Namespace string `json:"namespace"` | ||
IsBuildtimeDependency bool `json:"isBuildtimeDependency"` | ||
IsRuntimeDependency bool `json:"isRuntimeDependency"` | ||
LogURL string `json:"logURL"` | ||
SourceURI string `json:"sourceURI"` | ||
WasFixed bool `json:"wasFixed"` | ||
IsDependencyError bool `json:"isDependencyError"` | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Define types at the top level (outside of the function) so they can be referenced outside of the function, and to keep the function low on boilerplate.
for _, artifact := range failedArtifacts { | ||
jsonBytes, err := json.Marshal(ArtifactOutput{ | ||
Name: artifact.Name(), | ||
Version: artifact.Version(), | ||
Namespace: artifact.Ingredients[0].Namespace, | ||
IsBuildtimeDependency: artifact.IsBuildtimeDependency, | ||
IsRuntimeDependency: artifact.IsRuntimeDependency, | ||
LogURL: artifact.LogURL, | ||
SourceURI: artifact.Ingredients[0].IngredientSource.Url.String(), | ||
WasFixed: wasFixed[artifact.ArtifactID], | ||
IsDependencyError: isDependencyError[artifact.ArtifactID], | ||
}) | ||
if err != nil { | ||
return fmt.Errorf("error marshaling results: %w", err) | ||
} | ||
runner.output.Print(string(jsonBytes)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not asking for changes, just noting; Our output package handles structured output like this. But it's based on us running in either JSON or "plain" mode. ie. it affects ALL output. Not sure what this means for MCP, cause with MCP it's kind of a case by case.
Just noting it for now for awareness. Probably an area of improvement for later.
No description provided.