Skip to content

Commit 7699775

Browse files
authored
Add microsoft/apm dependencies support to frontmatter (#19883)
1 parent bac1f8a commit 7699775

15 files changed

+466
-5
lines changed

.github/aw/actions-lock.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,11 @@
169169
"repo": "super-linter/super-linter",
170170
"version": "v8.5.0",
171171
"sha": "61abc07d755095a68f4987d1c2c3d1d64408f1f9"
172+
},
173+
"microsoft/apm-action@v1": {
174+
"repo": "microsoft/apm-action",
175+
"version": "v1",
176+
"sha": "92d6dc8046ad61b340662adefd2f997bf93d2987"
172177
}
173178
}
174179
}

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,26 @@ plugins:
132132

133133
Each plugin repository must be specified in `org/repo` format. The compiler generates installation steps that run after the engine CLI is installed but before workflow execution begins.
134134

135+
### APM Dependencies (`dependencies:`)
136+
137+
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.
138+
139+
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.
140+
141+
```yaml wrap
142+
dependencies:
143+
- microsoft/apm-sample-package
144+
- github/awesome-copilot/skills/review-and-refactor
145+
- anthropics/skills/skills/frontend-design
146+
```
147+
148+
Each entry is an APM package reference. Supported formats:
149+
150+
- `owner/repo` — full APM package
151+
- `owner/repo/path/to/skill` — individual skill or primitive from a repository
152+
153+
The compiler generates an `Install APM dependencies` step that runs after the engine CLI installation steps.
154+
135155
### Runtimes (`runtimes:`)
136156

137157
Override default runtime versions for languages and tools used in workflows. The compiler automatically detects runtime requirements from tool configurations and workflow steps, then installs the specified versions.

pkg/parser/schemas/main_workflow_schema.json

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7516,6 +7516,16 @@
75167516
}
75177517
}
75187518
]
7519+
},
7520+
"dependencies": {
7521+
"description": "List of APM package references to install (e.g., 'org/repo' or 'org/repo/path/to/skill').",
7522+
"examples": [["microsoft/apm-sample-package", "acme/custom-tools"]],
7523+
"type": "array",
7524+
"items": {
7525+
"type": "string",
7526+
"pattern": "^[a-zA-Z0-9_-]+/[a-zA-Z0-9_./-]+$",
7527+
"description": "APM package reference in the format 'org/repo' or 'org/repo/path/to/skill'"
7528+
}
75197529
}
75207530
},
75217531
"additionalProperties": false,
@@ -8147,7 +8157,9 @@
81478157
"repositories": {
81488158
"type": "array",
81498159
"description": "Repositories to grant the token access to. Defaults to the current repository. Use [\"*\"] for org-wide access.",
8150-
"items": { "type": "string" },
8160+
"items": {
8161+
"type": "string"
8162+
},
81518163
"examples": [["repo-a", "repo-b"], ["*"]]
81528164
}
81538165
},

pkg/workflow/action_pins_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -297,9 +297,9 @@ func TestApplyActionPinToStep(t *testing.T) {
297297
func TestGetActionPinsSorting(t *testing.T) {
298298
pins := getActionPins()
299299

300-
// Verify we got all the pins (34 as of March 2026)
301-
if len(pins) != 34 {
302-
t.Errorf("getActionPins() returned %d pins, expected 34", len(pins))
300+
// Verify we got all the pins (35 as of March 2026)
301+
if len(pins) != 35 {
302+
t.Errorf("getActionPins() returned %d pins, expected 35", len(pins))
303303
}
304304

305305
// Verify they are sorted by version (descending) then by repository name (ascending)

pkg/workflow/apm_dependencies.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package workflow
2+
3+
import (
4+
"github.com/github/gh-aw/pkg/logger"
5+
)
6+
7+
var apmDepsLog = logger.New("workflow:apm_dependencies")
8+
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.
12+
//
13+
// Parameters:
14+
// - apmDeps: APM dependency configuration extracted from frontmatter
15+
// - data: WorkflowData used for action pin resolution
16+
//
17+
// Returns a GitHubActionStep, or an empty step if apmDeps is nil or has no packages.
18+
func GenerateAPMDependenciesStep(apmDeps *APMDependenciesInfo, data *WorkflowData) GitHubActionStep {
19+
if apmDeps == nil || len(apmDeps.Packages) == 0 {
20+
apmDepsLog.Print("No APM dependencies to install")
21+
return GitHubActionStep{}
22+
}
23+
24+
apmDepsLog.Printf("Generating APM dependencies step: %d packages", len(apmDeps.Packages))
25+
26+
// Resolve the pinned action reference for microsoft/apm-action.
27+
actionRef := GetActionPin("microsoft/apm-action")
28+
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.
31+
lines := []string{
32+
" - name: Install APM dependencies",
33+
" uses: " + actionRef,
34+
" with:",
35+
" dependencies: |",
36+
}
37+
38+
for _, dep := range apmDeps.Packages {
39+
lines = append(lines, " - "+dep)
40+
}
41+
42+
return GitHubActionStep(lines)
43+
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
//go:build integration
2+
3+
package workflow
4+
5+
import (
6+
"os"
7+
"path/filepath"
8+
"strings"
9+
"testing"
10+
11+
"github.com/github/gh-aw/pkg/testutil"
12+
"github.com/stretchr/testify/assert"
13+
"github.com/stretchr/testify/require"
14+
)
15+
16+
func TestAPMDependenciesCompilationSinglePackage(t *testing.T) {
17+
tmpDir := testutil.TempDir(t, "apm-deps-single-test")
18+
19+
workflow := `---
20+
engine: copilot
21+
on: workflow_dispatch
22+
permissions:
23+
issues: read
24+
pull-requests: read
25+
dependencies:
26+
- microsoft/apm-sample-package
27+
---
28+
29+
Test with a single APM dependency
30+
`
31+
32+
testFile := filepath.Join(tmpDir, "test-apm-single.md")
33+
err := os.WriteFile(testFile, []byte(workflow), 0644)
34+
require.NoError(t, err, "Failed to write test file")
35+
36+
compiler := NewCompiler()
37+
err = compiler.CompileWorkflow(testFile)
38+
require.NoError(t, err, "Compilation should succeed")
39+
40+
lockFile := strings.Replace(testFile, ".md", ".lock.yml", 1)
41+
content, err := os.ReadFile(lockFile)
42+
require.NoError(t, err, "Failed to read lock file")
43+
44+
lockContent := string(content)
45+
46+
assert.Contains(t, lockContent, "Install APM dependencies",
47+
"Lock file should contain APM dependencies step name")
48+
assert.Contains(t, lockContent, "microsoft/apm-action",
49+
"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")
52+
assert.Contains(t, lockContent, "- microsoft/apm-sample-package",
53+
"Lock file should list the dependency package")
54+
}
55+
56+
func TestAPMDependenciesCompilationMultiplePackages(t *testing.T) {
57+
tmpDir := testutil.TempDir(t, "apm-deps-multi-test")
58+
59+
workflow := `---
60+
engine: copilot
61+
on: workflow_dispatch
62+
permissions:
63+
issues: read
64+
pull-requests: read
65+
dependencies:
66+
- microsoft/apm-sample-package
67+
- github/awesome-copilot/skills/review-and-refactor
68+
- anthropics/skills/skills/frontend-design
69+
---
70+
71+
Test with multiple APM dependencies
72+
`
73+
74+
testFile := filepath.Join(tmpDir, "test-apm-multi.md")
75+
err := os.WriteFile(testFile, []byte(workflow), 0644)
76+
require.NoError(t, err, "Failed to write test file")
77+
78+
compiler := NewCompiler()
79+
err = compiler.CompileWorkflow(testFile)
80+
require.NoError(t, err, "Compilation should succeed")
81+
82+
lockFile := strings.Replace(testFile, ".md", ".lock.yml", 1)
83+
content, err := os.ReadFile(lockFile)
84+
require.NoError(t, err, "Failed to read lock file")
85+
86+
lockContent := string(content)
87+
88+
assert.Contains(t, lockContent, "Install APM dependencies",
89+
"Lock file should contain APM dependencies step name")
90+
assert.Contains(t, lockContent, "microsoft/apm-action",
91+
"Lock file should reference the microsoft/apm-action action")
92+
assert.Contains(t, lockContent, "- microsoft/apm-sample-package",
93+
"Lock file should include first dependency")
94+
assert.Contains(t, lockContent, "- github/awesome-copilot/skills/review-and-refactor",
95+
"Lock file should include second dependency")
96+
assert.Contains(t, lockContent, "- anthropics/skills/skills/frontend-design",
97+
"Lock file should include third dependency")
98+
}
99+
100+
func TestAPMDependenciesCompilationNoDependencies(t *testing.T) {
101+
tmpDir := testutil.TempDir(t, "apm-deps-none-test")
102+
103+
workflow := `---
104+
engine: copilot
105+
on: workflow_dispatch
106+
permissions:
107+
issues: read
108+
pull-requests: read
109+
---
110+
111+
Test without APM dependencies
112+
`
113+
114+
testFile := filepath.Join(tmpDir, "test-apm-none.md")
115+
err := os.WriteFile(testFile, []byte(workflow), 0644)
116+
require.NoError(t, err, "Failed to write test file")
117+
118+
compiler := NewCompiler()
119+
err = compiler.CompileWorkflow(testFile)
120+
require.NoError(t, err, "Compilation should succeed")
121+
122+
lockFile := strings.Replace(testFile, ".md", ".lock.yml", 1)
123+
content, err := os.ReadFile(lockFile)
124+
require.NoError(t, err, "Failed to read lock file")
125+
126+
lockContent := string(content)
127+
128+
assert.NotContains(t, lockContent, "Install APM dependencies",
129+
"Lock file should not contain APM dependencies step when no dependencies specified")
130+
assert.NotContains(t, lockContent, "microsoft/apm-action",
131+
"Lock file should not reference microsoft/apm-action when no dependencies specified")
132+
}

0 commit comments

Comments
 (0)