Skip to content

Commit 00a89bc

Browse files
committed
Simplify issue template generation
1 parent 9c8a6e4 commit 00a89bc

12 files changed

Lines changed: 86 additions & 280 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ The generated `source-sync` workflow stays thin: it checks out this repository a
121121

122122
If a project defines `[app.sync]` in its MSRA source, the generated sync step can preserve repo-specific runtime artifacts and ignore generated noise. For the FixPrice layout that means `tests/__snapshots__` stays in `main`, while `**/__pycache__` and `**/*.pyc` are removed before commit.
123123

124-
If a project defines `[app.issue_templates]` in its MSRA source, the generator also writes GitHub issue forms into `.github/ISSUE_TEMPLATE/` using the same source-driven contract.
124+
If a project defines `[app.issue_templates]` in its MSRA source, the generator also writes GitHub issue forms into `.github/ISSUE_TEMPLATE/`. The source contract only supplies `assignee`; the three forms are hardcoded in the generator, and `contact_links` are built from `package_owner`/`package_name` plus `app.social.discord` and `app.social.telegram` when present.
125125

126126
Before validation, the generated workflow installs the target project's `requirements-dev.txt` so the validation step runs with the generated dependencies available.
127127

docs/msra-app.md

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
| `timeout_ms` | Глобальный таймаут по умолчанию | Становится базовым лимитом для runtime-операций и используется, если локальный блок не переопределяет timeout. |
1919
| `browser` | Браузер по умолчанию | Определяет, какой движок запустит runtime для warmup и browser-backed функций. Если не указан, runtime использует `camoufox` по умолчанию. |
2020
| `@DisallowHeadless` | Запрещает headless-запуск | Меняет дефолт `headless` на `False`, заставляет runtime выбрасывать ошибку, если caller всё же просит `headless=True`, и переключает generated test pipeline на headed запуск под `xvfb-run`. |
21-
| `issue_templates` | GitHub issue forms | Если в source MSRA присутствует `[app.issue_templates]`, generator пишет `.github/ISSUE_TEMPLATE/` с `config.yml`, `bug_report.yml`, `documentation_issue.yml` и `feature_request.yml`. |
21+
| `issue_templates` | GitHub issue forms | Если в source MSRA присутствует `[app.issue_templates]`, generator пишет `.github/ISSUE_TEMPLATE/` с `config.yml`, `bug_report.yml`, `documentation_issue.yml` и `feature_request.yml`. Source-контракт ограничен `assignee`; остальные поля и сами формы зашиты в generator, а `contact_links` собираются из `package_owner`/`package_name` и `app.social`. |
2222
| `@Humanize` | Включает humanized-режим браузера | Передаёт `humanize=` в `AsyncCamoufox`, снижает “ботоподобность” поведения и требует `browser="camoufox"`. Разрешена либо как пустая аннотация, либо с положительным числом интенсивности, например `@Humanize(0.5)`. |
2323
| `@BlockImages` | Блокирует загрузку изображений | Передаёт `block_images=` в `AsyncCamoufox`, ускоряет старт и экономит трафик. Аннотация работает только с `browser="camoufox"`. |
2424

@@ -61,16 +61,15 @@
6161

6262
Если `[app.issue_templates]` присутствует в source MSRA, generator пишет GitHub issue forms в `.github/ISSUE_TEMPLATE/`.
6363

64-
Ожидаются:
64+
Ожидается только:
6565

66-
- `blank_issues_enabled`
6766
- `assignee`
68-
- `contact_links`
69-
- `bug_report`
70-
- `documentation_issue`
71-
- `feature_request`
7267

73-
Контракт целиком source-driven: если блок есть, генератор требует все перечисленные части и падает runtime error, если чего-то не хватает.
68+
Остальное генерируется из кода:
69+
70+
- `blank_issues_enabled: false`
71+
- формы `bug_report`, `documentation_issue` и `feature_request`
72+
- `contact_links`, где `Read the docs` строится из `package_owner`/`package_name`, а `Discord` и `Telegram` добавляются из `app.social.discord` и `app.social.telegram`, если они заданы
7473

7574
## Аннотации
7675

msra_codegen/issue_templates.py

Lines changed: 46 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -12,77 +12,67 @@
1212
def build_github_issue_templates_context(project: dict[str, Any]) -> dict[str, Any] | None:
1313
app = project.get("app", {})
1414
issue_templates = app.get("issue_templates")
15-
if not issue_templates:
15+
if issue_templates is None:
1616
return None
1717
if not isinstance(issue_templates, dict):
1818
raise RuntimeError("app.issue_templates must be a table.")
1919

20-
blank_issues_enabled = issue_templates.get("blank_issues_enabled")
21-
if not isinstance(blank_issues_enabled, bool):
22-
raise RuntimeError("app.issue_templates.blank_issues_enabled must be a boolean.")
23-
2420
assignee = str(issue_templates.get("assignee", "")).strip()
2521
if not assignee:
2622
raise RuntimeError("app.issue_templates.assignee must be a non-empty string.")
2723

28-
contact_links = issue_templates.get("contact_links")
29-
if not isinstance(contact_links, list) or not contact_links:
30-
raise RuntimeError("app.issue_templates.contact_links must be a non-empty list.")
31-
32-
normalized_contact_links: list[dict[str, str]] = []
33-
for index, link in enumerate(contact_links):
34-
if not isinstance(link, dict):
35-
raise RuntimeError(f"app.issue_templates.contact_links[{index}] must be a table.")
36-
name = str(link.get("name", "")).strip()
37-
url = str(link.get("url", "")).strip()
38-
about = str(link.get("about", "")).strip()
39-
if not name or not url or not about:
40-
raise RuntimeError(f"app.issue_templates.contact_links[{index}] must define name, url, and about.")
41-
normalized_contact_links.append(
42-
{
43-
"name": name,
44-
"url": url,
45-
"about": about,
46-
}
47-
)
48-
49-
templates: dict[str, dict[str, Any]] = {}
50-
for template_name in ("bug_report", "documentation_issue", "feature_request"):
51-
template_value = issue_templates.get(template_name)
52-
if not isinstance(template_value, dict):
53-
raise RuntimeError(f"app.issue_templates.{template_name} must be a table.")
54-
55-
name = str(template_value.get("name", "")).strip()
56-
description = str(template_value.get("description", "")).strip()
57-
title = str(template_value.get("title", "")).strip()
58-
labels = template_value.get("labels")
59-
if not isinstance(labels, list) or not labels:
60-
raise RuntimeError(f"app.issue_templates.{template_name}.labels must be a non-empty list.")
61-
normalized_labels: list[str] = []
62-
for index, label in enumerate(labels):
63-
text = str(label).strip()
64-
if not text:
65-
raise RuntimeError(f"app.issue_templates.{template_name}.labels[{index}] must be a non-empty string.")
66-
normalized_labels.append(text)
67-
if not name or not description or not title:
68-
raise RuntimeError(f"app.issue_templates.{template_name} must define name, description, and title.")
69-
70-
templates[template_name] = {
71-
"name": name,
72-
"description": description,
73-
"title": title,
74-
"labels": normalized_labels,
75-
}
24+
contact_links = build_issue_template_contact_links(app)
7625

7726
return {
78-
"blank_issues_enabled": blank_issues_enabled,
7927
"assignee": assignee,
80-
"contact_links": normalized_contact_links,
28+
"contact_links": contact_links,
8129
"yaml_value": lambda value: json.dumps(value, ensure_ascii=False),
82-
**templates,
8330
}
8431

8532

33+
def build_issue_template_contact_links(app: dict[str, Any]) -> list[dict[str, str]]:
34+
package_owner = str(app.get("package_owner", "")).strip()
35+
package_name = str(app.get("package_name", "")).strip()
36+
if not package_owner or not package_name:
37+
raise RuntimeError("app.package_owner and app.package_name are required to generate issue templates.")
38+
39+
contact_links: list[dict[str, str]] = [
40+
{
41+
"name": "📖 Read the docs",
42+
"url": f"https://{package_owner.lower()}.github.io/{package_name}/quick_start.html",
43+
"about": "Start here for “how-to” questions.",
44+
}
45+
]
46+
47+
social = app.get("social")
48+
if social is None:
49+
return contact_links
50+
if not isinstance(social, dict):
51+
raise RuntimeError("app.social must be a table when issue templates are enabled.")
52+
53+
discord_url = str(social.get("discord", "")).strip()
54+
if discord_url:
55+
contact_links.append(
56+
{
57+
"name": "💬 Discord server (Discussions)",
58+
"url": discord_url,
59+
"about": "General Q&A and community support.",
60+
}
61+
)
62+
63+
telegram_url = str(social.get("telegram", "")).strip()
64+
if telegram_url:
65+
contact_links.append(
66+
{
67+
"name": "💬 Telegram channel (Discussions)",
68+
"url": telegram_url,
69+
"about": "General Q&A and community support.",
70+
}
71+
)
72+
73+
return contact_links
74+
75+
8676
def generate_github_issue_templates_project(project: dict[str, Any], output_dir: Path) -> None:
8777
issue_templates_root = output_dir / ".github" / "ISSUE_TEMPLATE"
8878
context = build_github_issue_templates_context(project)

msra_codegen/project_model.py

Lines changed: 5 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -206,11 +206,12 @@ def get_assignment(table: dict[str, Any] | None, key: str, default: Any = None)
206206
table
207207
for table in tables
208208
if len(table["path"]) >= 3 and table["path"][0] == "app" and table["path"][1] == "issue_templates"
209+
and len(table["path"]) > 2
209210
]
210-
if issue_templates_child_tables and issue_templates_table is None:
211-
raise RuntimeError("app.issue_templates child tables require the root app.issue_templates table.")
211+
if issue_templates_child_tables:
212+
raise RuntimeError("app.issue_templates only supports the assignee field and no child tables.")
212213
if issue_templates_table:
213-
app["issue_templates"] = build_issue_templates_spec(tables, get_assignment)
214+
app["issue_templates"] = build_issue_templates_spec(issue_templates_table, get_assignment)
214215

215216
variable_tables = [
216217
table
@@ -460,92 +461,12 @@ def build_headers_spec(table: dict[str, Any] | None, get_assignment) -> dict[str
460461
}
461462

462463

463-
def build_issue_templates_spec(tables: list[dict[str, Any]], get_assignment) -> dict[str, Any]:
464-
root_table = None
465-
for table in tables:
466-
if table["path"] == ["app", "issue_templates"]:
467-
root_table = table
468-
break
469-
if root_table is None:
470-
raise RuntimeError("app.issue_templates is missing.")
471-
472-
blank_issues_enabled_value = get_assignment(root_table, "blank_issues_enabled")
473-
if blank_issues_enabled_value is None:
474-
raise RuntimeError('app.issue_templates.blank_issues_enabled is required.')
475-
blank_issues_enabled = bool(get_plain_value(blank_issues_enabled_value))
476-
464+
def build_issue_templates_spec(root_table: dict[str, Any], get_assignment) -> dict[str, Any]:
477465
assignee = str(get_plain_value(get_assignment(root_table, "assignee", ""))).strip()
478466
if not assignee:
479467
raise RuntimeError('app.issue_templates.assignee is required and must be a non-empty string.')
480-
481-
contact_links_value = get_plain_value(get_assignment(root_table, "contact_links", []))
482-
if not isinstance(contact_links_value, list):
483-
raise TypeError('app.issue_templates.contact_links must be a list of objects.')
484-
contact_links: list[dict[str, Any]] = []
485-
for index, item in enumerate(contact_links_value):
486-
if not isinstance(item, dict):
487-
raise TypeError(f'app.issue_templates.contact_links[{index}] must be an inline table.')
488-
name = str(get_plain_value(item.get("name", ""))).strip()
489-
url = str(get_plain_value(item.get("url", ""))).strip()
490-
about = str(get_plain_value(item.get("about", ""))).strip()
491-
if not name:
492-
raise RuntimeError(f'app.issue_templates.contact_links[{index}].name is required and must be non-empty.')
493-
if not url:
494-
raise RuntimeError(f'app.issue_templates.contact_links[{index}].url is required and must be non-empty.')
495-
if not about:
496-
raise RuntimeError(f'app.issue_templates.contact_links[{index}].about is required and must be non-empty.')
497-
contact_links.append(
498-
{
499-
"name": name,
500-
"url": url,
501-
"about": about,
502-
}
503-
)
504-
505-
if not contact_links:
506-
raise RuntimeError("app.issue_templates.contact_links must contain at least one entry.")
507-
508-
forms = {}
509-
for form_name in ("bug_report", "documentation_issue", "feature_request"):
510-
form_table = None
511-
for table in tables:
512-
if table["path"] == ["app", "issue_templates", form_name]:
513-
form_table = table
514-
break
515-
if form_table is None:
516-
raise RuntimeError(f'app.issue_templates.{form_name} is required.')
517-
name = str(get_plain_value(get_assignment(form_table, "name", ""))).strip()
518-
description = str(get_plain_value(get_assignment(form_table, "description", ""))).strip()
519-
title = str(get_plain_value(get_assignment(form_table, "title", ""))).strip()
520-
labels_value = get_plain_value(get_assignment(form_table, "labels", []))
521-
if not isinstance(labels_value, list):
522-
raise TypeError(f'app.issue_templates.{form_name}.labels must be a list of strings.')
523-
labels: list[str] = []
524-
for index, label in enumerate(labels_value):
525-
text = str(get_plain_value(label)).strip()
526-
if not text:
527-
raise RuntimeError(f'app.issue_templates.{form_name}.labels[{index}] must be a non-empty string.')
528-
labels.append(text)
529-
if not name:
530-
raise RuntimeError(f'app.issue_templates.{form_name}.name is required and must be non-empty.')
531-
if not description:
532-
raise RuntimeError(f'app.issue_templates.{form_name}.description is required and must be non-empty.')
533-
if not title:
534-
raise RuntimeError(f'app.issue_templates.{form_name}.title is required and must be non-empty.')
535-
if not labels:
536-
raise RuntimeError(f'app.issue_templates.{form_name}.labels must contain at least one entry.')
537-
forms[form_name] = {
538-
"name": name,
539-
"description": description,
540-
"title": title,
541-
"labels": labels,
542-
}
543-
544468
return {
545-
"blank_issues_enabled": blank_issues_enabled,
546469
"assignee": assignee,
547-
"contact_links": contact_links,
548-
**forms,
549470
}
550471

551472

msra_codegen/templates/github/issue_templates/bug_report.yml.tpl

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
name: {{ yaml_value(bug_report.name) }}
2-
description: {{ yaml_value(bug_report.description) }}
3-
title: {{ yaml_value(bug_report.title) }}
4-
labels: {{ yaml_value(bug_report.labels) }}
1+
name: "🐛 Bug report"
2+
description: Report something that isn’t working as intended
3+
title: "[Bug] <short title>"
4+
labels: ["bug"]
55
assignees: [{{ yaml_value(assignee) }}]
66

77
body:

msra_codegen/templates/github/issue_templates/config.yml.tpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
blank_issues_enabled: {{ "true" if blank_issues_enabled else "false" }}
1+
blank_issues_enabled: false
22

33
contact_links:
44
{% for link in contact_links %}

msra_codegen/templates/github/issue_templates/documentation_issue.yml.tpl

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
name: {{ yaml_value(documentation_issue.name) }}
2-
description: {{ yaml_value(documentation_issue.description) }}
3-
title: {{ yaml_value(documentation_issue.title) }}
4-
labels: {{ yaml_value(documentation_issue.labels) }}
1+
name: "📚 Docs issue"
2+
description: Flag inaccurate or missing documentation
3+
title: "[Docs] <short title>"
4+
labels: ["documentation"]
55
assignees: [{{ yaml_value(assignee) }}]
66

77
body:

msra_codegen/templates/github/issue_templates/feature_request.yml.tpl

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
name: {{ yaml_value(feature_request.name) }}
2-
description: {{ yaml_value(feature_request.description) }}
3-
title: {{ yaml_value(feature_request.title) }}
4-
labels: {{ yaml_value(feature_request.labels) }}
1+
name: "✨ Feature request"
2+
description: Suggest an idea to improve the project
3+
title: "[Feature] <short title>"
4+
labels: ["feature", "enhancement"]
55
assignees: [{{ yaml_value(assignee) }}]
66

77
body:

tests/codegen.test.js

Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1390,7 +1390,7 @@ test("readme pipeline uses example type=image instead of function name", () => {
13901390
}
13911391
});
13921392

1393-
test("github issue templates are generated from app.issue_templates", () => {
1393+
test("github issue templates are generated from assignee-only app.issue_templates", () => {
13941394
const repoRoot = path.resolve(__dirname, "..");
13951395
const workDir = mkdtempSync(path.join(os.tmpdir(), "msra-issue-templates-"));
13961396
const inputPath = path.join(workDir, "issue-templates.msra");
@@ -1401,32 +1401,10 @@ test("github issue templates are generated from app.issue_templates", () => {
14011401
'@BlockImages # по умолч false',
14021402
[
14031403
'@BlockImages # по умолч false',
1404+
'social={telegram="https://t.me/miskler_dev", discord="https://discord.gg/UnJnGHNbBp"}',
14041405
"",
14051406
"[app.issue_templates]",
1406-
"blank_issues_enabled=false",
14071407
'assignee="miskler"',
1408-
'contact_links=[',
1409-
' { name="📖 Read the docs", url="https://open-inflation.github.io/chizhik_api/quick_start.html", about="Start here for “how-to” questions." },',
1410-
' { name="💬 Discord server (Discussions)", url="https://discord.gg/UnJnGHNbBp", about="General Q&A and community support." }',
1411-
"]",
1412-
"",
1413-
"[app.issue_templates.bug_report]",
1414-
'name="🐛 Bug report"',
1415-
'description="Report something that isn’t working as intended"',
1416-
'title="[Bug] <short title>"',
1417-
'labels=["bug"]',
1418-
"",
1419-
"[app.issue_templates.documentation_issue]",
1420-
'name="📚 Docs issue"',
1421-
'description="Flag inaccurate or missing documentation"',
1422-
'title="[Docs] <short title>"',
1423-
'labels=["documentation"]',
1424-
"",
1425-
"[app.issue_templates.feature_request]",
1426-
'name="✨ Feature request"',
1427-
'description="Suggest an idea to improve the project"',
1428-
'title="[Feature] <short title>"',
1429-
'labels=["feature", "enhancement"]',
14301408
"",
14311409
].join("\n"),
14321410
);
@@ -1455,11 +1433,19 @@ test("github issue templates are generated from app.issue_templates", () => {
14551433

14561434
assert.match(configText, /blank_issues_enabled: false/);
14571435
assert.match(configText, /name: "📖 Read the docs"/);
1458-
assert.match(configText, /url: "https:\/\/open-inflation\.github\.io\/chizhik_api\/quick_start\.html"/);
1436+
assert.match(configText, /url:\s+"https:\/\/miskler\.github\.io\/ozon_api\/quick_start\.html"/);
14591437
assert.match(configText, /name: "💬 Discord server \(Discussions\)"/);
1438+
assert.match(configText, /url: "https:\/\/discord\.gg\/UnJnGHNbBp"/);
1439+
assert.match(configText, /name: "💬 Telegram channel \(Discussions\)"/);
1440+
assert.match(configText, /url: "https:\/\/t\.me\/miskler_dev"/);
1441+
assert.match(bugReportText, /name: "🐛 Bug report"/);
14601442
assert.match(bugReportText, /assignees: \["miskler"\]/);
14611443
assert.match(bugReportText, /labels: \["bug"\]/);
1444+
assert.match(docsIssueText, /name: "📚 Docs issue"/);
1445+
assert.match(docsIssueText, /assignees: \["miskler"\]/);
14621446
assert.match(docsIssueText, /labels: \["documentation"\]/);
1447+
assert.match(featureRequestText, /name: " Feature request"/);
1448+
assert.match(featureRequestText, /assignees: \["miskler"\]/);
14631449
assert.match(featureRequestText, /labels: \["feature", "enhancement"\]/);
14641450
} finally {
14651451
rmSync(workDir, { recursive: true, force: true });

0 commit comments

Comments
 (0)