Skip to content

Commit a2f65f9

Browse files
authored
Add Copilot-only lsp frontmatter support with schema, codegen, engine guardrails, and LSP instructions (experimental) (#41777)
1 parent 7bbb734 commit a2f65f9

25 files changed

Lines changed: 1273 additions & 32 deletions

.github/aw/lsp.md

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
---
2+
description: Language Server Protocol (LSP) configuration reference for gh-aw Copilot workflows — frontmatter syntax, supported servers, and file extension mapping.
3+
---
4+
5+
# LSP Configuration
6+
7+
> ⚠️ **Experimental feature.** The `lsp` frontmatter field is experimental. Using it will emit a compile-time warning. The interface may change in future releases.
8+
9+
The `lsp` frontmatter field lets Copilot-engine workflows declare language servers. At compile time, the compiler:
10+
11+
1. Validates the configuration and rejects the workflow if `lsp` is used with a non-Copilot engine.
12+
2. Generates `~/.copilot/settings.json` with an `lspServers` block the Copilot CLI reads at startup.
13+
3. Injects install steps for known server ecosystems into the agent setup job.
14+
15+
> ⚠️ **`lsp` is only supported with `engine: copilot`**. Using it with any other engine causes a compile-time error.
16+
17+
## Syntax
18+
19+
```yaml
20+
engine:
21+
id: copilot
22+
23+
lsp:
24+
<language-key>:
25+
command: <server-executable>
26+
args: [<arg1>, <arg2>] # optional
27+
fileExtensions:
28+
".<ext>": <language-id> # at least one required
29+
```
30+
31+
Each key under `lsp` is a language identifier (lowercase, alphanumeric, hyphens, or underscores). It maps to a server definition with:
32+
33+
| Field | Required | Description |
34+
|---|---|---|
35+
| `command` | **yes** | Executable name or path for the language server |
36+
| `args` | no | Command-line arguments passed to the server on startup |
37+
| `fileExtensions` | **yes** | Map of file extension (with leading `.`) to LSP language ID |
38+
| `version` | no | Package version to install (e.g. `"5.8.3"`). Overrides the built-in pinned default for known servers. |
39+
40+
## Built-in Servers
41+
42+
For the languages below, the compiler automatically injects an install step — no manual `steps:` entry is needed. Each server is pinned to a known-good release by default; use the `version` field to override.
43+
44+
| Language key | Default version | Install command | Example `command` |
45+
|---|---|---|---|
46+
| `bash` | `5.4.0` | `npm install -g --ignore-scripts bash-language-server@5.4.0` | `bash-language-server` |
47+
| `go` | `0.18.1` | `go install golang.org/x/tools/gopls@v0.18.1` | `gopls` |
48+
| `php` | `1.14.1` | `npm install -g --ignore-scripts intelephense@1.14.1` | `intelephense` |
49+
| `python` | `1.1.399` | `npm install -g --ignore-scripts pyright@1.1.399` | `pyright-langserver` |
50+
| `ruby` | `0.50.0` | `gem install solargraph -v 0.50.0` | `solargraph` |
51+
| `rust` | n/a | `rustup component add rust-analyzer` | `rust-analyzer` |
52+
| `typescript` | `5.8.3` / `4.3.3` | `npm install -g --ignore-scripts typescript@5.8.3 typescript-language-server@4.3.3` | `typescript-language-server` |
53+
| `yaml` | `1.15.0` | `npm install -g --ignore-scripts yaml-language-server@1.15.0` | `yaml-language-server` |
54+
55+
> The `version` field overrides the pinned version for the primary language server package (the last package in the install list). For `typescript`, it controls `typescript-language-server`; `typescript` itself stays at its hardcoded companion version (`5.8.3`).
56+
57+
Language keys not in this table still work — the compiler simply skips the auto-install step. Add a manual `steps:` entry to install the server yourself.
58+
59+
## Examples
60+
61+
### TypeScript / JavaScript
62+
63+
```yaml
64+
engine:
65+
id: copilot
66+
67+
lsp:
68+
typescript:
69+
command: typescript-language-server
70+
args: ["--stdio"]
71+
fileExtensions:
72+
".ts": typescript
73+
".tsx": typescriptreact
74+
".js": javascript
75+
".cjs": javascript
76+
".mjs": javascript
77+
```
78+
79+
### Python
80+
81+
```yaml
82+
engine:
83+
id: copilot
84+
85+
lsp:
86+
python:
87+
command: pyright-langserver
88+
args: ["--stdio"]
89+
fileExtensions:
90+
".py": python
91+
```
92+
93+
### Go
94+
95+
```yaml
96+
engine:
97+
id: copilot
98+
99+
lsp:
100+
go:
101+
command: gopls
102+
fileExtensions:
103+
".go": go
104+
```
105+
106+
### Multiple Languages
107+
108+
```yaml
109+
engine:
110+
id: copilot
111+
112+
lsp:
113+
typescript:
114+
command: typescript-language-server
115+
args: ["--stdio"]
116+
fileExtensions:
117+
".ts": typescript
118+
".js": javascript
119+
python:
120+
command: pyright-langserver
121+
args: ["--stdio"]
122+
fileExtensions:
123+
".py": python
124+
```
125+
126+
### Custom Server (no built-in install)
127+
128+
For servers without a built-in install spec, add a manual install step:
129+
130+
```yaml
131+
engine:
132+
id: copilot
133+
134+
steps:
135+
- name: Install custom language server
136+
run: npm install -g my-custom-language-server
137+
138+
lsp:
139+
mylang:
140+
command: my-custom-language-server
141+
args: ["--stdio"]
142+
fileExtensions:
143+
".ml": mylang
144+
```
145+
146+
## Network Requirements
147+
148+
Installing LSP servers requires network access to the appropriate package registry. Add the matching ecosystem to `network.allowed`:
149+
150+
| Language | Ecosystem to add |
151+
|---|---|
152+
| `bash`, `php`, `python`, `typescript`, `yaml` | `node` |
153+
| `go` | `go` |
154+
| `ruby` | `ruby` |
155+
| `rust` | `rust` |
156+
157+
```yaml
158+
network:
159+
allowed:
160+
- node # for npm-installed servers (typescript, yaml, python/pyright, etc.)
161+
- go # for gopls
162+
```
163+
164+
## Compile-time Validation
165+
166+
The compiler enforces these rules at compile time:
167+
168+
- `lsp` requires `engine: copilot` — any other engine causes an error.
169+
- Each language entry must have a non-empty `command`.
170+
- Each language entry must define at least one `fileExtensions` mapping.
171+
- Language keys are case-insensitive and trimmed; duplicate keys that collapse to the same lowercase value cause nondeterministic behavior and should be avoided.

.github/skills/agentic-workflows/SKILL.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Load these files from `github/gh-aw` (they are not available locally).
2929
- `.github/aw/github-mcp-server.md`
3030
- `.github/aw/llms.md`
3131
- `.github/aw/loop.md`
32+
- `.github/aw/lsp.md`
3233
- `.github/aw/mcp-clis.md`
3334
- `.github/aw/memory-stateful-patterns.md`
3435
- `.github/aw/memory.md`

.github/workflows/jsweep.lock.yml

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

.github/workflows/jsweep.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@ tools:
2525
edit:
2626
bash: ["*"]
2727
cache-memory: true
28+
lsp:
29+
typescript:
30+
command: typescript-language-server
31+
args: ["--stdio"]
32+
fileExtensions:
33+
".js": javascript
34+
".cjs": javascript
35+
".mjs": javascript
36+
".ts": typescript
37+
".tsx": typescriptreact
2838
steps:
2939
- name: Install Node.js dependencies
3040
working-directory: actions/setup/js

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

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

.github/workflows/smoke-copilot.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,21 @@ tools:
5555
mode: cli
5656
web-fetch:
5757
cli-proxy: true
58+
lsp:
59+
typescript:
60+
command: typescript-language-server
61+
args: ["--stdio"]
62+
fileExtensions:
63+
".js": javascript
64+
".cjs": javascript
65+
".mjs": javascript
66+
".ts": typescript
67+
".tsx": typescriptreact
5868
runtimes:
5969
go:
6070
version: "1.26"
71+
node:
72+
version: "20"
6173
models:
6274
providers:
6375
anthropic:
@@ -189,14 +201,18 @@ Run these checks and mark each as ✅/❌:
189201
13. Comment memory: append an original 3-line haiku to `/tmp/gh-aw/comment-memory/*.md`.
190202
14. Sub-agent: use `file-summarizer` on `README.md`.
191203
15. Check run: call `create_check_run` with `conclusion=success`, title `Smoke Copilot - Run ${{ github.run_id }}`, summary `All smoke tests completed.`, text `Detailed results attached.`
204+
16. **LSP TypeScript Testing**: Use the TypeScript language server (configured via `lsp.typescript` frontmatter) to count the number of functions in `${{ github.workspace }}/actions/setup/js/safe_output_helpers.cjs`:
205+
- Open the file `${{ github.workspace }}/actions/setup/js/safe_output_helpers.cjs` via LSP
206+
- Use LSP document symbols to list all symbols in the file and count functions
207+
- Report the total function count as ✅ if at least 1 function is found, ❌ otherwise
192208

193209
## Output
194210

195211
1. **Create an issue** with a summary of the smoke test run:
196212
- Use the temporary ID `aw_smoke1` for the issue so you can reference it later
197213
- Title: "Smoke Test: Copilot - ${{ github.run_id }}"
198214
- Body should include:
199-
- Test results (✅ or ❌ for each test)
215+
- Test results (✅ or ❌ for each test, including test #16 LSP TypeScript)
200216
- Overall status: PASS or FAIL
201217
- Run URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
202218
- Timestamp

pkg/cli/data/agentic_workflows_fallback_aw_files.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"github-mcp-server.md",
1818
"llms.md",
1919
"loop.md",
20+
"lsp.md",
2021
"mcp-clis.md",
2122
"memory-stateful-patterns.md",
2223
"memory.md",

pkg/parser/schema_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,29 @@ func TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_ToolsEditBoolean(t
491491
}
492492
}
493493

494+
func TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_LSPConfig(t *testing.T) {
495+
t.Parallel()
496+
497+
frontmatter := map[string]any{
498+
"on": "push",
499+
"engine": "copilot",
500+
"lsp": map[string]any{
501+
"typescript": map[string]any{
502+
"command": "typescript-language-server",
503+
"args": []any{"--stdio"},
504+
"fileExtensions": map[string]any{
505+
".ts": "typescript",
506+
},
507+
},
508+
},
509+
}
510+
511+
err := ValidateMainWorkflowFrontmatterWithSchemaAndLocation(frontmatter, "/tmp/gh-aw/lsp-config-test.md")
512+
if err != nil {
513+
t.Fatalf("expected valid lsp configuration to pass schema validation, got: %v", err)
514+
}
515+
}
516+
494517
func TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_MaxLimitsAllowExpressions(t *testing.T) {
495518
t.Parallel()
496519

0 commit comments

Comments
 (0)