Skip to content

Add plugin imports and merging support#14376

Merged
pelikhan merged 2 commits intomainfrom
copilot/fix-plugin-imports
Feb 7, 2026
Merged

Add plugin imports and merging support#14376
pelikhan merged 2 commits intomainfrom
copilot/fix-plugin-imports

Conversation

Copy link
Contributor

Copilot AI commented Feb 7, 2026

Plugins defined in imported workflows were not being extracted or installed in the compiled workflow. Only top-level plugin definitions were processed.

Changes

  • Parser: Added extractPluginsFromContent() to extract plugins from imported markdown files, following the same pattern as bots/labels extraction
  • Import Processing: Added MergedPlugins field to ImportsResult and implemented plugin collection with deduplication during import traversal
  • Compiler: Merged imported plugins with top-level plugins in compiler_orchestrator_tools.go, ensuring deduplication across all sources

Example

Before:

# shared-plugins.md
---
plugins:
  - github/plugin-a
---
# workflow.md
---
imports:
  - shared-plugins.md
plugins:
  - github/plugin-b
---

Previously, only plugin-b would be installed. Now both plugin-a (from import) and plugin-b (from main workflow) are installed, with duplicates automatically deduplicated.

Original prompt

This section details on the original issue you should resolve

<issue_title>Plugin imports</issue_title>
<issue_description>Ensure plugins are properly imported and merged when parsing a workflow</issue_description>

Comments on the Issue (you are @copilot in this section)


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Add support for importing and merging plugins from imported workflows:
- Add extractPluginsFromContent() in parser/content_extractor.go
- Add MergedPlugins field to ImportsResult struct
- Update import processor to extract and merge plugins from imports
- Merge imported plugins with top-level plugins in compiler orchestrator
- Deduplicate plugins across imports and top-level definitions
- Add comprehensive test suite for plugin imports

Fixes plugin imports issue where plugins from imported workflows
were not being included in the compiled workflow.

Co-authored-by: patrickcarnahan <17889693+patrickcarnahan@users.noreply.github.com>
Copilot AI changed the title [WIP] Ensure proper import and merging of plugins in workflow parsing Add plugin imports and merging support Feb 7, 2026
Copilot AI requested a review from patrickcarnahan February 7, 2026 15:34
@pelikhan pelikhan marked this pull request as ready for review February 7, 2026 15:36
Copilot AI review requested due to automatic review settings February 7, 2026 15:36
@pelikhan pelikhan merged commit 5ec97a6 into main Feb 7, 2026
152 checks passed
@pelikhan pelikhan deleted the copilot/fix-plugin-imports branch February 7, 2026 15:37
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds end-to-end support for discovering plugins declared in imported workflow markdown files and ensuring those plugins are included in the compiled lock workflow (alongside any top-level plugins).

Changes:

  • Added plugin extraction from imported markdown frontmatter during import traversal (MergedPlugins).
  • Merged imported plugins with top-level plugins in the compiler with deduplication.
  • Added unit tests verifying plugin installation steps appear in the compiled lock file for imported plugins (including dedup scenarios).

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
pkg/workflow/plugin_import_test.go New tests asserting imported plugins are installed and duplicates are deduplicated.
pkg/workflow/compiler_orchestrator_tools.go Merges importsResult.MergedPlugins with top-level plugins and deduplicates.
pkg/parser/import_processor.go Collects plugins from imported markdown files into MergedPlugins.
pkg/parser/content_extractor.go Adds extractPluginsFromContent() helper to read plugins from imported frontmatter.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +148 to +150
// extractPluginsFromContent extracts plugins section from frontmatter as JSON string
func extractPluginsFromContent(content string) (string, error) {
return extractFrontmatterField(content, "plugins", "[]")
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extractPluginsFromContent returns the raw JSON for the plugins frontmatter field. Since plugins also supports the object form ({ repos: [...], github-token: ... }), callers that expect a JSON array (as in import processing) will silently fail to extract any plugins when the object form is used. Consider making this helper return a normalized JSON array of repos (matching the array form) or introducing a dedicated extractor that always yields []string repos regardless of whether the source was array or object format.

Suggested change
// extractPluginsFromContent extracts plugins section from frontmatter as JSON string
func extractPluginsFromContent(content string) (string, error) {
return extractFrontmatterField(content, "plugins", "[]")
// extractPluginsFromContent extracts plugins section from frontmatter as a JSON array of repos.
// It normalizes both the array form:
// plugins: [ "owner1/repo1", "owner2/repo2" ]
// and the object form:
// plugins:
// repos: [ "owner1/repo1", "owner2/repo2" ]
// github-token: ...
// into a JSON array of strings: ["owner1/repo1","owner2/repo2"].
func extractPluginsFromContent(content string) (string, error) {
contentExtractorLog.Printf("Extracting plugins")
result, err := ExtractFrontmatterFromContent(content)
if err != nil {
contentExtractorLog.Printf("Failed to extract frontmatter for plugins: %v", err)
return "[]", nil
}
rawPlugins, exists := result.Frontmatter["plugins"]
if !exists {
contentExtractorLog.Printf("Plugins field not found in frontmatter")
return "[]", nil
}
// Helper to convert various slice types to []string.
toStringSlice := func(v interface{}) []string {
switch vv := v.(type) {
case []string:
return vv
case []interface{}:
out := make([]string, 0, len(vv))
for _, item := range vv {
if s, ok := item.(string); ok {
out = append(out, s)
}
}
return out
default:
return nil
}
}
var repos []string
switch v := rawPlugins.(type) {
case []interface{}, []string:
// Already an array form; normalize to []string.
repos = toStringSlice(v)
case map[string]interface{}:
if rv, ok := v["repos"]; ok {
repos = toStringSlice(rv)
}
case map[interface{}]interface{}:
// Handle maps with non-string keys from YAML parsing.
for key, value := range v {
if ks, ok := key.(string); ok && ks == "repos" {
repos = toStringSlice(value)
break
}
}
default:
contentExtractorLog.Printf("Plugins field has unsupported type %T", rawPlugins)
}
if len(repos) == 0 {
contentExtractorLog.Printf("No repos found in plugins configuration")
return "[]", nil
}
pluginsJSON, err := json.Marshal(repos)
if err != nil {
contentExtractorLog.Printf("Failed to marshal plugins repos to JSON: %v", err)
return "[]", nil
}
contentExtractorLog.Printf("Successfully extracted plugins: count=%d", len(repos))
return strings.TrimSpace(string(pluginsJSON)), nil

Copilot uses AI. Check for mistakes.
Comment on lines +553 to +560
// Parse plugins JSON array
var importedPlugins []string
if jsonErr := json.Unmarshal([]byte(pluginsContent), &importedPlugins); jsonErr == nil {
for _, plugin := range importedPlugins {
if !pluginsSet[plugin] {
pluginsSet[plugin] = true
plugins = append(plugins, plugin)
}
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Imported plugin extraction assumes plugins frontmatter is a JSON array and unmarshals into []string. If an imported workflow uses the supported object format (plugins: { repos: [...], github-token: ... }), json.Unmarshal will fail and the plugins will be dropped with no error, so imports won’t behave the same as top-level parsing. Consider parsing the imported file’s frontmatter plugins value as either []any or map[string]any (like extractPluginsFromFrontmatter does) and extracting repos accordingly (and decide how to handle/merge an imported github-token if present).

Suggested change
// Parse plugins JSON array
var importedPlugins []string
if jsonErr := json.Unmarshal([]byte(pluginsContent), &importedPlugins); jsonErr == nil {
for _, plugin := range importedPlugins {
if !pluginsSet[plugin] {
pluginsSet[plugin] = true
plugins = append(plugins, plugin)
}
// Parse plugins JSON which may be either:
// - a JSON array of strings: ["owner/repo", ...]
// - a JSON object: { "repos": ["owner/repo", ...], "github-token": "..." }
var importedPluginsRaw any
if jsonErr := json.Unmarshal([]byte(pluginsContent), &importedPluginsRaw); jsonErr == nil {
switch v := importedPluginsRaw.(type) {
case []any:
for _, item := range v {
plugin, ok := item.(string)
if !ok {
continue
}
if !pluginsSet[plugin] {
pluginsSet[plugin] = true
plugins = append(plugins, plugin)
}
}
case map[string]any:
if reposRaw, ok := v["repos"]; ok {
if reposSlice, ok := reposRaw.([]any); ok {
for _, item := range reposSlice {
plugin, ok := item.(string)
if !ok {
continue
}
if !pluginsSet[plugin] {
pluginsSet[plugin] = true
plugins = append(plugins, plugin)
}
}
}
}
// Note: imported "github-token" (if present) is intentionally ignored here
// to avoid changing existing token-handling behavior.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Plugin imports

3 participants