Skip to content
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/smoke-copilot.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 6 additions & 3 deletions .github/workflows/smoke-copilot.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,12 @@ engine:
id: copilot
max-continuations: 2
imports:
- shared/gh.md
- shared/reporting.md
- shared/github-queries-mcp-script.md
aw:
- shared/gh.md
- shared/reporting.md
- shared/github-queries-mcp-script.md
plugins:
Copy link
Contributor

Choose a reason for hiding this comment

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

The plugins import for secret-scanning is a good addition to the smoke test. This validates the marketplace plugin install flow end-to-end.

- https://github.com/github/copilot-plugins/tree/main/plugins/advanced-security/skills/secret-scanning
network:
allowed:
- defaults
Expand Down
230 changes: 230 additions & 0 deletions pkg/cli/compile_imports_marketplaces_plugins_integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
//go:build integration

package cli

import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)

// TestCompileCopilotImportsMarketplacesPlugins compiles the canonical Copilot workflow
// that uses imports.marketplaces and imports.plugins and verifies that the compiled
// lock file contains the correct `copilot plugin marketplace add` and
// `copilot plugin install` setup steps before the agent execution step.
func TestCompileCopilotImportsMarketplacesPlugins(t *testing.T) {
setup := setupIntegrationTest(t)
defer setup.cleanup()

Copy link
Contributor

Choose a reason for hiding this comment

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

Good test structure using setupIntegrationTest. Consider adding a test for the case where a plugin URL is provided without a marketplace URL to ensure error handling is robust.

Copy link
Contributor

Choose a reason for hiding this comment

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

Good use of setupIntegrationTest helper for consistent test setup and cleanup. The test clearly validates the full marketplace plugin compilation flow end-to-end.

srcPath := filepath.Join(projectRoot, "pkg/cli/workflows/test-copilot-imports-marketplaces-plugins.md")
dstPath := filepath.Join(setup.workflowsDir, "test-copilot-imports-marketplaces-plugins.md")

srcContent, err := os.ReadFile(srcPath)
if err != nil {
t.Fatalf("Failed to read source workflow file %s: %v", srcPath, err)
}
if err := os.WriteFile(dstPath, srcContent, 0644); err != nil {
t.Fatalf("Failed to write workflow to test dir: %v", err)
}

cmd := exec.Command(setup.binaryPath, "compile", dstPath)
output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("CLI compile command failed: %v\nOutput: %s", err, string(output))
}

lockFilePath := filepath.Join(setup.workflowsDir, "test-copilot-imports-marketplaces-plugins.lock.yml")
lockContent, err := os.ReadFile(lockFilePath)
if err != nil {
t.Fatalf("Failed to read lock file: %v", err)
}
lockContentStr := string(lockContent)

// Verify marketplace registration step
if !strings.Contains(lockContentStr, "copilot plugin marketplace add https://marketplace.example.com") {
t.Errorf("Lock file should contain marketplace add step\nLock file content:\n%s", lockContentStr)
}

// Verify plugin install step
if !strings.Contains(lockContentStr, "copilot plugin install my-plugin") {
t.Errorf("Lock file should contain plugin install step\nLock file content:\n%s", lockContentStr)
}

// Verify marketplace step appears before the agent execution step (sudo -E awf / run: copilot)
marketplaceIdx := strings.Index(lockContentStr, "copilot plugin marketplace add")
pluginInstallIdx := strings.Index(lockContentStr, "copilot plugin install my-plugin")
agentExecIdx := strings.Index(lockContentStr, "sudo -E awf")
if marketplaceIdx == -1 || pluginInstallIdx == -1 || agentExecIdx == -1 {
t.Fatalf("Could not find all expected steps: marketplace=%d, plugin=%d, agent=%d",
marketplaceIdx, pluginInstallIdx, agentExecIdx)
}
if marketplaceIdx >= agentExecIdx || pluginInstallIdx >= agentExecIdx {
t.Errorf("Marketplace/plugin steps should appear before the agent execution step")
}

t.Logf("Copilot marketplace/plugins workflow compiled successfully to %s", lockFilePath)
}

// TestCompileClaudeImportsMarketplacesPlugins compiles the canonical Claude workflow
// that uses imports.marketplaces and imports.plugins and verifies that the compiled
// lock file contains the correct `claude plugin marketplace add` and
// `claude plugin install` setup steps before the agent execution step.
func TestCompileClaudeImportsMarketplacesPlugins(t *testing.T) {
setup := setupIntegrationTest(t)
defer setup.cleanup()

srcPath := filepath.Join(projectRoot, "pkg/cli/workflows/test-claude-imports-marketplaces-plugins.md")
dstPath := filepath.Join(setup.workflowsDir, "test-claude-imports-marketplaces-plugins.md")

srcContent, err := os.ReadFile(srcPath)
if err != nil {
t.Fatalf("Failed to read source workflow file %s: %v", srcPath, err)
}
if err := os.WriteFile(dstPath, srcContent, 0644); err != nil {
t.Fatalf("Failed to write workflow to test dir: %v", err)
}

cmd := exec.Command(setup.binaryPath, "compile", dstPath)
output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("CLI compile command failed: %v\nOutput: %s", err, string(output))
}

lockFilePath := filepath.Join(setup.workflowsDir, "test-claude-imports-marketplaces-plugins.lock.yml")
lockContent, err := os.ReadFile(lockFilePath)
if err != nil {
t.Fatalf("Failed to read lock file: %v", err)
}
lockContentStr := string(lockContent)

// Verify marketplace registration step
if !strings.Contains(lockContentStr, "claude plugin marketplace add https://marketplace.example.com") {
t.Errorf("Lock file should contain marketplace add step\nLock file content:\n%s", lockContentStr)
}

// Verify plugin install step
if !strings.Contains(lockContentStr, "claude plugin install my-plugin") {
t.Errorf("Lock file should contain plugin install step\nLock file content:\n%s", lockContentStr)
}

t.Logf("Claude marketplace/plugins workflow compiled successfully to %s", lockFilePath)
}

// TestCompileCopilotImportsMarketplacesPluginsShared compiles the canonical Copilot
// workflow that imports a shared workflow (via imports.aw) which defines its own
// marketplaces and plugins, and verifies that the values are merged and deduplicated
// in the generated lock file.
func TestCompileCopilotImportsMarketplacesPluginsShared(t *testing.T) {
setup := setupIntegrationTest(t)
defer setup.cleanup()

// Copy both the shared fixture and the main workflow into the test dir
sharedDir := filepath.Join(setup.workflowsDir, "shared")
if err := os.MkdirAll(sharedDir, 0755); err != nil {
t.Fatalf("Failed to create shared dir: %v", err)
}

sharedSrc := filepath.Join(projectRoot, "pkg/cli/workflows/shared/marketplace-plugins.md")
sharedDst := filepath.Join(sharedDir, "marketplace-plugins.md")
sharedContent, err := os.ReadFile(sharedSrc)
if err != nil {
t.Fatalf("Failed to read shared workflow file %s: %v", sharedSrc, err)
}
if err := os.WriteFile(sharedDst, sharedContent, 0644); err != nil {
t.Fatalf("Failed to write shared workflow: %v", err)
}

srcPath := filepath.Join(projectRoot, "pkg/cli/workflows/test-copilot-imports-marketplaces-plugins-shared.md")
dstPath := filepath.Join(setup.workflowsDir, "test-copilot-imports-marketplaces-plugins-shared.md")
srcContent, err := os.ReadFile(srcPath)
if err != nil {
t.Fatalf("Failed to read source workflow file %s: %v", srcPath, err)
}
if err := os.WriteFile(dstPath, srcContent, 0644); err != nil {
t.Fatalf("Failed to write workflow to test dir: %v", err)
}

cmd := exec.Command(setup.binaryPath, "compile", dstPath)
output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("CLI compile command failed: %v\nOutput: %s", err, string(output))
}

lockFilePath := filepath.Join(setup.workflowsDir, "test-copilot-imports-marketplaces-plugins-shared.lock.yml")
lockContent, err := os.ReadFile(lockFilePath)
if err != nil {
t.Fatalf("Failed to read lock file: %v", err)
}
lockContentStr := string(lockContent)

// Main workflow's own marketplace and plugin should be present
if !strings.Contains(lockContentStr, "copilot plugin marketplace add https://main-marketplace.example.com") {
t.Errorf("Lock file should contain main marketplace add step\nLock file content:\n%s", lockContentStr)
}
if !strings.Contains(lockContentStr, "copilot plugin install main-plugin") {
t.Errorf("Lock file should contain main plugin install step\nLock file content:\n%s", lockContentStr)
}

// Shared workflow's marketplace and plugin should also be present (merged)
if !strings.Contains(lockContentStr, "copilot plugin marketplace add https://shared-marketplace.example.com") {
t.Errorf("Lock file should contain shared marketplace add step\nLock file content:\n%s", lockContentStr)
}
if !strings.Contains(lockContentStr, "copilot plugin install shared-plugin") {
t.Errorf("Lock file should contain shared plugin install step\nLock file content:\n%s", lockContentStr)
}

// Values should appear exactly once (deduplication)
if count := strings.Count(lockContentStr, "copilot plugin marketplace add https://main-marketplace.example.com"); count != 1 {
t.Errorf("Main marketplace step should appear exactly once, got %d\nLock file content:\n%s", count, lockContentStr)
}
if count := strings.Count(lockContentStr, "copilot plugin install main-plugin"); count != 1 {
t.Errorf("Main plugin step should appear exactly once, got %d\nLock file content:\n%s", count, lockContentStr)
}

t.Logf("Copilot shared marketplace/plugins workflow compiled successfully to %s", lockFilePath)
}

// TestCompileCodexImportsMarketplacesPluginsError verifies that using imports.marketplaces
// or imports.plugins with the Codex engine fails compilation with a clear error.
func TestCompileCodexImportsMarketplacesPluginsError(t *testing.T) {
setup := setupIntegrationTest(t)
defer setup.cleanup()

const workflowContent = `---
on: issues
permissions:
contents: read
issues: read
engine: codex
imports:
marketplaces:
- https://marketplace.example.com
plugins:
- my-plugin
---

# Test Codex Imports Marketplaces and Plugins

Process the issue.
`
dstPath := filepath.Join(setup.workflowsDir, "test-codex-unsupported-imports.md")
if err := os.WriteFile(dstPath, []byte(workflowContent), 0644); err != nil {
t.Fatalf("Failed to write workflow: %v", err)
}

cmd := exec.Command(setup.binaryPath, "compile", dstPath)
output, err := cmd.CombinedOutput()
outputStr := string(output)

if err == nil {
t.Fatalf("Expected compile to fail for Codex with imports.marketplaces/plugins, but it succeeded\nOutput: %s", outputStr)
}

if !strings.Contains(outputStr, "imports.marketplaces") {
t.Errorf("Error output should mention 'imports.marketplaces'\nOutput: %s", outputStr)
}

t.Logf("Correctly rejected Codex workflow with imports.marketplaces/plugins: %s", outputStr)
}
9 changes: 9 additions & 0 deletions pkg/cli/workflows/shared/marketplace-plugins.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
imports:
marketplaces:
- https://shared-marketplace.example.com
plugins:
- shared-plugin
---

This shared workflow provides marketplace and plugin configuration for testing imports merging.
19 changes: 19 additions & 0 deletions pkg/cli/workflows/test-claude-imports-marketplaces-plugins.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
on: issues
permissions:
contents: read
issues: read
engine: claude
imports:
marketplaces:
- https://marketplace.example.com
plugins:
- my-plugin
---

# Test Claude Imports Marketplaces and Plugins

This workflow tests that `imports.marketplaces` and `imports.plugins` are compiled into
`claude plugin marketplace add` and `claude plugin install` setup steps before the agent runs.

Process the issue and respond with a helpful comment.
Copy link
Contributor

Choose a reason for hiding this comment

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

Clean and minimal test workflow — the prompt is concise and the frontmatter correctly exercises both imports.marketplaces and imports.plugins for the Claude engine.

Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
on: issues
permissions:
contents: read
issues: read
engine: copilot
imports:
aw:
- shared/marketplace-plugins.md
marketplaces:
- https://main-marketplace.example.com
plugins:
- main-plugin
---

# Test Copilot Imports Marketplaces and Plugins with Shared Import

This workflow tests that `imports.marketplaces` and `imports.plugins` values from a shared
agentic workflow (imported via `imports.aw`) are merged with the main workflow's own values.

Process the issue and respond with a helpful comment.
19 changes: 19 additions & 0 deletions pkg/cli/workflows/test-copilot-imports-marketplaces-plugins.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
on: issues
permissions:
contents: read
issues: read
engine: copilot
imports:
marketplaces:
- https://marketplace.example.com
plugins:
- my-plugin
---

# Test Copilot Imports Marketplaces and Plugins

This workflow tests that `imports.marketplaces` and `imports.plugins` are compiled into
`copilot plugin marketplace add` and `copilot plugin install` setup steps before the agent runs.

Process the issue and respond with a helpful comment.
2 changes: 1 addition & 1 deletion pkg/parser/import_bfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func processImportsFromFrontmatterWithManifestAndSource(frontmatter map[string]a
}
}
default:
return nil, errors.New("imports field must be an array or an object with 'aw'/'apm-packages' subfields")
return nil, errors.New("imports field must be an array or an object with 'aw', 'apm-packages', 'marketplaces', or 'plugins' subfields")
}

if len(importSpecs) == 0 {
Expand Down
Loading
Loading