Skip to content

Commit f5e3621

Browse files
authored
Merge branch 'main' into copilot/replace-inlined-engines-with-yml-resources
2 parents c536da1 + 9122b7f commit f5e3621

14 files changed

Lines changed: 457 additions & 77 deletions

.changeset/patch-package-apm-artifacts.md

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.github/workflows/smoke-claude.lock.yml

Lines changed: 28 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.github/workflows/smoke-claude.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ tools:
4949
serena:
5050
languages:
5151
go: {}
52+
dependencies:
53+
- microsoft/apm-sample-package
5254
runtimes:
5355
go:
5456
version: "1.25"

docs/src/content/docs/reference/frontmatter.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,23 +151,35 @@ Each plugin repository must be specified in `org/repo` format. The compiler gene
151151

152152
### APM Dependencies (`dependencies:`)
153153

154-
Specifies [microsoft/apm](https://github.com/microsoft/apm) packages to install before workflow execution. When present, the compiler emits a step using the `microsoft/apm-action` action to install the listed packages.
154+
Specifies [microsoft/apm](https://github.com/microsoft/apm) packages to install before workflow execution. When present, the compiler resolves and packs dependencies in the activation job, then unpacks them in the agent job for faster, deterministic startup.
155155

156156
APM (Agent Package Manager) manages AI agent primitives such as skills, prompts, instructions, agents, and hooks. Packages can depend on other packages and APM resolves the full dependency tree.
157157

158+
**Simple array format** (most common):
159+
158160
```yaml wrap
159161
dependencies:
160162
- microsoft/apm-sample-package
161163
- github/awesome-copilot/skills/review-and-refactor
162164
- anthropics/skills/skills/frontend-design
163165
```
164166

167+
**Object format** with options:
168+
169+
```yaml wrap
170+
dependencies:
171+
packages:
172+
- microsoft/apm-sample-package
173+
- github/awesome-copilot/skills/review-and-refactor
174+
isolated: true # clear repo primitives before unpack (default: false)
175+
```
176+
165177
Each entry is an APM package reference. Supported formats:
166178

167179
- `owner/repo` — full APM package
168180
- `owner/repo/path/to/skill` — individual skill or primitive from a repository
169181

170-
The compiler generates an `Install APM dependencies` step that runs after the engine CLI installation steps.
182+
The compiler emits a pack step in the activation job and a restore step in the agent job. The APM target is automatically inferred from the configured engine (`copilot`, `claude`, or `all` for other engines). The `isolated` flag controls whether existing `.github/` primitive directories are cleared before the bundle is unpacked in the agent job.
171183

172184
### Runtimes (`runtimes:`)
173185

pkg/workflow/agentic_engine.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,11 @@ type WorkflowExecutor interface {
148148
// before secret redaction runs. Engines that copy session or firewall state files should
149149
// override this; the default implementation returns an empty slice.
150150
GetFirewallLogsCollectionStep(workflowData *WorkflowData) []GitHubActionStep
151+
152+
// GetAPMTarget returns the APM target value to use when packing dependencies with
153+
// microsoft/apm-action. Supported values are "copilot", "claude", and "all".
154+
// The default implementation returns "all" (packs all primitive types).
155+
GetAPMTarget() string
151156
}
152157

153158
// MCPConfigProvider handles MCP (Model Context Protocol) configuration
@@ -337,6 +342,12 @@ func (e *BaseEngine) GetFirewallLogsCollectionStep(workflowData *WorkflowData) [
337342
return []GitHubActionStep{}
338343
}
339344

345+
// GetAPMTarget returns "all" by default (packs all primitive types).
346+
// CopilotEngine overrides this to return "copilot"; ClaudeEngine overrides to return "claude".
347+
func (e *BaseEngine) GetAPMTarget() string {
348+
return "all"
349+
}
350+
340351
// ParseLogMetrics provides a default no-op implementation for log parsing
341352
// Engines can override this to provide detailed log parsing and metrics extraction
342353
func (e *BaseEngine) ParseLogMetrics(logContent string, verbose bool) LogMetrics {

pkg/workflow/apm_dependencies.go

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,30 +6,29 @@ import (
66

77
var apmDepsLog = logger.New("workflow:apm_dependencies")
88

9-
// GenerateAPMDependenciesStep generates a GitHub Actions step that installs APM packages
10-
// using the microsoft/apm-action action. The step is emitted when the workflow frontmatter
11-
// contains a non-empty `dependencies` list in microsoft/apm format.
9+
// GenerateAPMPackStep generates the GitHub Actions step that installs APM packages and
10+
// packs them into a bundle in the activation job. The step always uses isolated:true because
11+
// the activation job has no repo context to preserve.
1212
//
1313
// Parameters:
1414
// - apmDeps: APM dependency configuration extracted from frontmatter
15-
// - data: WorkflowData used for action pin resolution
15+
// - target: APM target derived from the agentic engine (e.g. "copilot", "claude", "all")
16+
// - data: WorkflowData used for action pin resolution
1617
//
1718
// Returns a GitHubActionStep, or an empty step if apmDeps is nil or has no packages.
18-
func GenerateAPMDependenciesStep(apmDeps *APMDependenciesInfo, data *WorkflowData) GitHubActionStep {
19+
func GenerateAPMPackStep(apmDeps *APMDependenciesInfo, target string, data *WorkflowData) GitHubActionStep {
1920
if apmDeps == nil || len(apmDeps.Packages) == 0 {
20-
apmDepsLog.Print("No APM dependencies to install")
21+
apmDepsLog.Print("No APM dependencies to pack")
2122
return GitHubActionStep{}
2223
}
2324

24-
apmDepsLog.Printf("Generating APM dependencies step: %d packages", len(apmDeps.Packages))
25+
apmDepsLog.Printf("Generating APM pack step: %d packages, target=%s", len(apmDeps.Packages), target)
2526

26-
// Resolve the pinned action reference for microsoft/apm-action.
2727
actionRef := GetActionPin("microsoft/apm-action")
2828

29-
// Build step lines. The `dependencies` input uses a YAML block scalar (`|`)
30-
// so each package is written as an indented list item on its own line.
3129
lines := []string{
32-
" - name: Install APM dependencies",
30+
" - name: Install and pack APM dependencies",
31+
" id: apm_pack",
3332
" uses: " + actionRef,
3433
" with:",
3534
" dependencies: |",
@@ -39,5 +38,45 @@ func GenerateAPMDependenciesStep(apmDeps *APMDependenciesInfo, data *WorkflowDat
3938
lines = append(lines, " - "+dep)
4039
}
4140

41+
lines = append(lines,
42+
" isolated: 'true'",
43+
" pack: 'true'",
44+
" archive: 'true'",
45+
" target: "+target,
46+
" working-directory: /tmp/gh-aw/apm-workspace",
47+
)
48+
49+
return GitHubActionStep(lines)
50+
}
51+
52+
// GenerateAPMRestoreStep generates the GitHub Actions step that restores APM packages
53+
// from a pre-packed bundle in the agent job.
54+
//
55+
// Parameters:
56+
// - apmDeps: APM dependency configuration extracted from frontmatter
57+
// - data: WorkflowData used for action pin resolution
58+
//
59+
// Returns a GitHubActionStep, or an empty step if apmDeps is nil or has no packages.
60+
func GenerateAPMRestoreStep(apmDeps *APMDependenciesInfo, data *WorkflowData) GitHubActionStep {
61+
if apmDeps == nil || len(apmDeps.Packages) == 0 {
62+
apmDepsLog.Print("No APM dependencies to restore")
63+
return GitHubActionStep{}
64+
}
65+
66+
apmDepsLog.Printf("Generating APM restore step (isolated=%v)", apmDeps.Isolated)
67+
68+
actionRef := GetActionPin("microsoft/apm-action")
69+
70+
lines := []string{
71+
" - name: Restore APM dependencies",
72+
" uses: " + actionRef,
73+
" with:",
74+
" bundle: /tmp/gh-aw/apm-bundle/*.tar.gz",
75+
}
76+
77+
if apmDeps.Isolated {
78+
lines = append(lines, " isolated: 'true'")
79+
}
80+
4281
return GitHubActionStep(lines)
4382
}

pkg/workflow/apm_dependencies_compilation_test.go

Lines changed: 110 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,37 @@ Test with a single APM dependency
4343

4444
lockContent := string(content)
4545

46-
assert.Contains(t, lockContent, "Install APM dependencies",
47-
"Lock file should contain APM dependencies step name")
46+
// Activation job should have the pack step
47+
assert.Contains(t, lockContent, "Install and pack APM dependencies",
48+
"Lock file should contain APM pack step in activation job")
4849
assert.Contains(t, lockContent, "microsoft/apm-action",
4950
"Lock file should reference the microsoft/apm-action action")
50-
assert.Contains(t, lockContent, "dependencies: |",
51-
"Lock file should use block scalar for dependencies input")
5251
assert.Contains(t, lockContent, "- microsoft/apm-sample-package",
5352
"Lock file should list the dependency package")
53+
assert.Contains(t, lockContent, "id: apm_pack",
54+
"Lock file should have apm_pack step ID")
55+
assert.Contains(t, lockContent, "pack: 'true'",
56+
"Lock file should include pack input")
57+
assert.Contains(t, lockContent, "target: copilot",
58+
"Lock file should include target inferred from copilot engine")
59+
60+
// Separate APM artifact upload in activation job
61+
assert.Contains(t, lockContent, "Upload APM bundle artifact",
62+
"Lock file should upload APM bundle as separate artifact")
63+
assert.Contains(t, lockContent, "name: apm",
64+
"Lock file should name the APM artifact 'apm'")
65+
66+
// Agent job should have download + restore steps
67+
assert.Contains(t, lockContent, "Download APM bundle artifact",
68+
"Lock file should download APM bundle in agent job")
69+
assert.Contains(t, lockContent, "Restore APM dependencies",
70+
"Lock file should contain APM restore step in agent job")
71+
assert.Contains(t, lockContent, "bundle: /tmp/gh-aw/apm-bundle/*.tar.gz",
72+
"Lock file should restore from bundle path")
73+
74+
// Old install step should NOT appear
75+
assert.NotContains(t, lockContent, "Install APM dependencies",
76+
"Lock file should not contain the old install step name")
5477
}
5578

5679
func TestAPMDependenciesCompilationMultiplePackages(t *testing.T) {
@@ -85,8 +108,8 @@ Test with multiple APM dependencies
85108

86109
lockContent := string(content)
87110

88-
assert.Contains(t, lockContent, "Install APM dependencies",
89-
"Lock file should contain APM dependencies step name")
111+
assert.Contains(t, lockContent, "Install and pack APM dependencies",
112+
"Lock file should contain APM pack step")
90113
assert.Contains(t, lockContent, "microsoft/apm-action",
91114
"Lock file should reference the microsoft/apm-action action")
92115
assert.Contains(t, lockContent, "- microsoft/apm-sample-package",
@@ -95,6 +118,8 @@ Test with multiple APM dependencies
95118
"Lock file should include second dependency")
96119
assert.Contains(t, lockContent, "- anthropics/skills/skills/frontend-design",
97120
"Lock file should include third dependency")
121+
assert.Contains(t, lockContent, "Restore APM dependencies",
122+
"Lock file should contain APM restore step")
98123
}
99124

100125
func TestAPMDependenciesCompilationNoDependencies(t *testing.T) {
@@ -125,8 +150,85 @@ Test without APM dependencies
125150

126151
lockContent := string(content)
127152

128-
assert.NotContains(t, lockContent, "Install APM dependencies",
129-
"Lock file should not contain APM dependencies step when no dependencies specified")
153+
assert.NotContains(t, lockContent, "Install and pack APM dependencies",
154+
"Lock file should not contain APM pack step when no dependencies specified")
155+
assert.NotContains(t, lockContent, "Restore APM dependencies",
156+
"Lock file should not contain APM restore step when no dependencies specified")
130157
assert.NotContains(t, lockContent, "microsoft/apm-action",
131158
"Lock file should not reference microsoft/apm-action when no dependencies specified")
132159
}
160+
161+
func TestAPMDependenciesCompilationObjectFormatIsolated(t *testing.T) {
162+
tmpDir := testutil.TempDir(t, "apm-deps-isolated-test")
163+
164+
workflow := `---
165+
engine: copilot
166+
on: workflow_dispatch
167+
permissions:
168+
issues: read
169+
pull-requests: read
170+
dependencies:
171+
packages:
172+
- microsoft/apm-sample-package
173+
isolated: true
174+
---
175+
176+
Test with isolated APM dependencies
177+
`
178+
179+
testFile := filepath.Join(tmpDir, "test-apm-isolated.md")
180+
err := os.WriteFile(testFile, []byte(workflow), 0644)
181+
require.NoError(t, err, "Failed to write test file")
182+
183+
compiler := NewCompiler()
184+
err = compiler.CompileWorkflow(testFile)
185+
require.NoError(t, err, "Compilation should succeed")
186+
187+
lockFile := strings.Replace(testFile, ".md", ".lock.yml", 1)
188+
content, err := os.ReadFile(lockFile)
189+
require.NoError(t, err, "Failed to read lock file")
190+
191+
lockContent := string(content)
192+
193+
assert.Contains(t, lockContent, "Install and pack APM dependencies",
194+
"Lock file should contain APM pack step")
195+
assert.Contains(t, lockContent, "Restore APM dependencies",
196+
"Lock file should contain APM restore step")
197+
// Restore step should include isolated: true because frontmatter says so
198+
assert.Contains(t, lockContent, "isolated: 'true'",
199+
"Lock file restore step should include isolated flag")
200+
}
201+
202+
func TestAPMDependenciesCompilationClaudeEngineTarget(t *testing.T) {
203+
tmpDir := testutil.TempDir(t, "apm-deps-claude-test")
204+
205+
workflow := `---
206+
engine: claude
207+
on: workflow_dispatch
208+
permissions:
209+
issues: read
210+
pull-requests: read
211+
dependencies:
212+
- microsoft/apm-sample-package
213+
---
214+
215+
Test with Claude engine target inference
216+
`
217+
218+
testFile := filepath.Join(tmpDir, "test-apm-claude.md")
219+
err := os.WriteFile(testFile, []byte(workflow), 0644)
220+
require.NoError(t, err, "Failed to write test file")
221+
222+
compiler := NewCompiler()
223+
err = compiler.CompileWorkflow(testFile)
224+
require.NoError(t, err, "Compilation should succeed")
225+
226+
lockFile := strings.Replace(testFile, ".md", ".lock.yml", 1)
227+
content, err := os.ReadFile(lockFile)
228+
require.NoError(t, err, "Failed to read lock file")
229+
230+
lockContent := string(content)
231+
232+
assert.Contains(t, lockContent, "target: claude",
233+
"Lock file should use claude target for claude engine")
234+
}

0 commit comments

Comments
 (0)