Copilot 会读取两类仓库自定义指令:仓库级的
.github/copilot-instructions.md,以及路径级的.github/instructions/**/NAME.instructions.md(通过 frontmatter 里的applyTo匹配文件)。 这里最容易踩的坑是 glob 的语义。按官方文档,*只匹配当前目录,src/*.py不会匹配src/foo/bar.py。这和很多人熟悉的 Pythonfnmatch不一样。 这篇写一个instructions_lint.py,按文档的 glob 语义检查每个applyTo实际匹配了哪些文件,并检查文件名、frontmatter 和excludeAgent的取值。
先看清成品(box 上真实运行)
- 文档里列出的 15 个 glob 示例,按文档描述逐条测试,全部通过。
- 示例仓库查出 3 个 ERROR:文件名不是
.instructions.md、缺少 frontmatter、excludeAgent: "coding-agent"不是合法值。- 还有 1 个 WARN:
applyTo: "*.py"只匹配了根目录下的两个脚本,src/和tests/里的 3 个.py文件都没有匹配到。- pytest 16 项通过。
合规提醒:GitHub Copilot 需要有效的订阅或组织授权,不要共享账号,也不要用代理绕过地区或组织的限制。Copilot 会把代码上下文发送给 GitHub 和它的模型服务商处理,公司仓库先确认组织策略(比如是否允许使用 cloud agent,是否开启公开代码匹配过滤)。仓库里的说明文件、workflow、agent 配置都不要写密钥,MCP 用到的密钥放在 Agents secrets 里。本文不列价格,以官网为准。
运行说明:box 上没有登录 GitHub,也没有 Copilot 订阅,所以 Copilot 实际是否读取这些文件、cloud agent 是否真的执行了 setup 步骤,都没有验证。本文的规则全部来自 docs.github.com 的原文(文中会标出处),检查脚本、actionlint 和测试都在 box 上真实运行过。
做完之后你会得到什么?
| 成果 | 说明 |
|---|---|
.github/copilot-instructions.md |
仓库级说明:技术栈、安装、测试、检查命令 |
.github/instructions/ |
两个正确的示例、三个故意写错的反例 |
instructions_lint.py |
检查脚本,有 ERROR 时退出码为 1 |
test_instructions_lint.py |
16 项测试(15 个文档 glob 示例加上示例仓库) |
需要准备哪些环境?
Python 3.9 以上,pip install pyyaml pytest。
官方规则(来自 add-repository-instructions 文档)
| 规则 | 内容 |
|---|---|
| 仓库级 | .github/copilot-instructions.md |
| 路径级 | .github/instructions 目录及其子目录下的 NAME.instructions.md,文件名必须以 .instructions.md 结尾 |
applyTo |
写在 frontmatter 里的 glob,多个模式用逗号分隔,比如 "**/*.ts,**/*.tsx" |
| 同时匹配 | 路径级和仓库级同时生效时,两者都会用上 |
excludeAgent |
可选,取值 "code-review" 或 "cloud-agent";不写的话,两者都会使用这份说明 |
AGENTS.md |
可以放在仓库任何位置,离当前文件最近的那个优先 |
| 优先级 | 个人说明 > 仓库说明 > 组织说明 |
文件结构
58-github-copilot-A-src/
├── .github/
│ ├── copilot-instructions.md
│ └── instructions/
│ ├── backend/python.instructions.md
│ ├── frontend.instructions.md
│ ├── review.instructions.md # 反例:excludeAgent 写错,*.py 只匹配根目录
│ ├── docs.md # 反例:文件名不对
│ └── no-frontmatter.instructions.md # 反例:没有 frontmatter
├── src/, tests/, web/ # 空文件,供 applyTo 匹配
├── instructions_lint.py
└── test_instructions_lint.py
第 1 步:说明文件
# 项目说明(给 Copilot)
这是一个 Python 3.12 + FastAPI 的订单服务,前端是 TypeScript。
- 安装依赖:`uv sync`
- 运行测试:`uv run pytest -q`
- 代码检查:`uv run ruff check .`
- 提交前必须让测试和 ruff 都通过。
<!-- .github/instructions/backend/python.instructions.md -->
---
applyTo: "src/**/*.py,tests/**/*.py"
---
- 函数都加类型注解
- 错误统一抛 `AppError(code, message)`
<!-- .github/instructions/frontend.instructions.md -->
---
applyTo: "web/*.ts,web/*.tsx"
excludeAgent: "code-review"
---
- 组件用函数式写法
<!-- .github/instructions/review.instructions.md(反例) -->
---
applyTo: "*.py"
excludeAgent: "coding-agent"
---
- 审查时重点看 SQL 注入
第 2 步:检查脚本
"""按 GitHub 官方文档检查 Copilot 仓库自定义指令。退出码:有 ERROR 为 1。
依据(docs.github.com/en/copilot/how-tos/configure-custom-instructions/add-repository-instructions):
- 仓库级:.github/copilot-instructions.md
- 路径级:.github/instructions/ 下(含子目录)的 NAME.instructions.md,文件名必须以 .instructions.md 结尾
- frontmatter 里用 applyTo 指定 glob,多个模式用逗号分隔
- glob 语义:* 只匹配当前目录;** 递归;src/*.py 不匹配 src/foo/bar.py
- excludeAgent 可选,取值 "code-review" 或 "cloud-agent"
"""
import pathlib, re, sys
import yaml
ROOT = pathlib.Path(".")
EXCLUDE = {"code-review", "cloud-agent"}
def glob_to_regex(pat):
i, out = 0, ""
while i < len(pat):
if pat.startswith("**/", i):
out += r"(?:.*/)?"; i += 3
elif pat.startswith("**", i):
out += r".*"; i += 2
elif pat[i] == "*":
out += r"[^/]*"; i += 1
elif pat[i] == "?":
out += r"[^/]"; i += 1
else:
out += re.escape(pat[i]); i += 1
return re.compile(out + r"\Z")
def matches(pat, path):
return bool(glob_to_regex(pat).match(path))
def lint(root=ROOT):
out = []
files = [p.relative_to(root).as_posix() for p in root.rglob("*")
if p.is_file() and ".git" not in p.parts and not p.relative_to(root).as_posix().startswith(".github/")]
if not (root / ".github/copilot-instructions.md").exists():
out.append(("INFO", ".github/copilot-instructions.md", "没有仓库级说明"))
idir = root / ".github/instructions"
for p in sorted(idir.rglob("*.md")) if idir.exists() else []:
rel = p.relative_to(root).as_posix()
if not p.name.endswith(".instructions.md"):
out.append(("ERROR", rel, "文件名必须以 .instructions.md 结尾,否则不会被使用")); continue
m = re.match(r"^---\n(.*?)\n---\n", p.read_text(encoding="utf-8"), re.S)
if not m:
out.append(("ERROR", rel, "缺少包含 applyTo 的 frontmatter")); continue
try:
fm = yaml.safe_load(m.group(1)) or {}
except yaml.YAMLError as e:
out.append(("ERROR", rel, f"frontmatter YAML 无效:{str(e).splitlines()[0]}")); continue
apply = fm.get("applyTo")
if not apply:
out.append(("ERROR", rel, "缺少 applyTo")); continue
ex = fm.get("excludeAgent")
if ex is not None and ex not in EXCLUDE:
out.append(("ERROR", rel, f"excludeAgent={ex!r} 无效,只能是 code-review 或 cloud-agent"))
for k in set(fm) - {"applyTo", "excludeAgent"}:
out.append(("WARN", rel, f"未知字段 {k}"))
for pat in [x.strip() for x in str(apply).split(",") if x.strip()]:
hits = [f for f in files if matches(pat, f)]
if not hits:
out.append(("WARN", rel, f"applyTo 模式 {pat} 没有匹配到任何文件"))
else:
out.append(("OK", rel, f"{pat} → {len(hits)} 个文件,如 {hits[0]}"))
if "/" not in pat: # 没有 / 的模式只匹配根目录,子目录里的同类文件全部漏掉
deeper = [f for f in files if "/" in f and matches(pat, f.rsplit("/", 1)[1])]
if deeper:
out.append(("WARN", rel, f"{pat} 只匹配根目录;子目录里还有 {len(deeper)} 个同类文件(如 {deeper[0]}),想递归请写 **/{pat}"))
return out
if __name__ == "__main__":
res = lint(pathlib.Path(sys.argv[1]) if len(sys.argv) > 1 else ROOT)
for lvl, f, msg in res:
print(f"[{lvl}] {f}: {msg}")
sys.exit(1 if any(l == "ERROR" for l, _, _ in res) else 0)
第 3 步:测试
import pytest
from instructions_lint import matches, lint
# 官方文档列出的 glob 示例
@pytest.mark.parametrize("pat,path,ok", [
("*", "README.md", True), ("*", "src/a.py", False),
("**", "src/a/b.py", True), ("**/*", "src/a/b.py", True),
("*.py", "a.py", True), ("*.py", "src/a.py", False),
("**/*.py", "a.py", True), ("**/*.py", "src/a/b.py", True),
("src/*.py", "src/foo.py", True), ("src/*.py", "src/foo/bar.py", False),
("src/**/*.py", "src/foo.py", True), ("src/**/*.py", "src/foo/bar/baz.py", True),
("**/subdir/**/*.py", "subdir/foo.py", True), ("**/subdir/**/*.py", "deep/parent/subdir/nested/qux.py", True),
("**/subdir/**/*.py", "foo.py", False),
])
def test_doc_glob_examples(pat, path, ok):
assert matches(pat, path) is ok
def test_sample_repo():
res = {(l, f, m) for l, f, m in lint()}
msgs = " | ".join(f"{l} {f} {m}" for l, f, m in res)
assert "ERROR .github/instructions/docs.md 文件名必须以 .instructions.md 结尾" in msgs
assert "ERROR .github/instructions/no-frontmatter.instructions.md 缺少包含 applyTo" in msgs
assert "excludeAgent='coding-agent' 无效" in msgs
assert "*.py 只匹配根目录;子目录里还有 3 个同类文件" in msgs
assert "OK .github/instructions/backend/python.instructions.md src/**/*.py → 2 个文件" in msgs
第 4 步:运行
$ python instructions_lint.py
[OK] .github/instructions/backend/python.instructions.md: src/**/*.py → 2 个文件,如 src/api/orders.py
[OK] .github/instructions/backend/python.instructions.md: tests/**/*.py → 1 个文件,如 tests/test_orders.py
[ERROR] .github/instructions/docs.md: 文件名必须以 .instructions.md 结尾,否则不会被使用
[OK] .github/instructions/frontend.instructions.md: web/*.ts → 1 个文件,如 web/app.ts
[OK] .github/instructions/frontend.instructions.md: web/*.tsx → 1 个文件,如 web/App.tsx
[ERROR] .github/instructions/no-frontmatter.instructions.md: 缺少包含 applyTo 的 frontmatter
[ERROR] .github/instructions/review.instructions.md: excludeAgent='coding-agent' 无效,只能是 code-review 或 cloud-agent
[OK] .github/instructions/review.instructions.md: *.py → 2 个文件,如 instructions_lint.py
[WARN] .github/instructions/review.instructions.md: *.py 只匹配根目录;子目录里还有 3 个同类文件(如 tests/test_orders.py),想递归请写 **/*.py
$ python -m pytest -q
16 passed in 0.02s
调试示例
1. *.py 匹配到的不是我以为的文件。 我原本预期 review.instructions.md 里的 applyTo: "*.py" 什么都匹配不到,测试也是这样写的,但测试失败了。实际输出是 *.py → 2 个文件,如 instructions_lint.py:它匹配到了根目录下的两个检查脚本,而真正需要审查的 src/、tests/ 里的文件一个都没匹配到。这正好符合文档的语义(*.py 只匹配当前目录)。所以“匹配到 0 个文件”并不是唯一的错误信号,我新增了一条 WARN:模式里没有 /,并且子目录里还有同类文件时提示改成 **/*.py。
2. 在 pytest 里,脚本检查的根本不是仓库。 改完之后测试还是失败,报错显示只检查了 INFO .github/copilot-instructions.md 没有仓库级说明。原因是 ROOT = pathlib.Path(sys.argv[1]) 写在了模块顶层,而在 pytest 里 sys.argv[1] 是测试文件的路径。改成只在 __main__ 里读取命令行参数后,16 项测试全部通过。
常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
| 路径级说明从来没有生效 | 文件名没有以 .instructions.md 结尾,或者不在 .github/instructions 下 |
改名或移动文件 |
src/*.py 漏掉了子目录 |
* 不跨目录 |
改用 src/**/*.py |
| 代码审查也用了只想给 cloud agent 的说明 | 没有写 excludeAgent |
加上 excludeAgent: "code-review" |
| 不确定说明有没有被用上 | — | 在 Copilot Chat 的回答里展开 References,看是否列出了说明文件 |
| 说明之间互相矛盾 | 个人、仓库、组织三层都写了 | 按优先级整理,避免冲突 |
模板规则
- 仓库级说明写构建、测试、检查命令,并且每条命令都要实际验证过。
- 路径级说明用
**递归匹配,用检查脚本确认每个模式至少匹配到一个文件。 - 检查脚本放进 CI。
项目资源
| 用途 | 地址 |
|---|---|
| Copilot 文档首页 | https://docs.github.com/en/copilot |
| 添加仓库自定义指令 | https://docs.github.com/en/copilot/how-tos/configure-custom-instructions/add-repository-instructions |
说明文件写对了,Copilot 在每个目录下都会按团队的约定写代码和审查代码。