#!/usr/bin/env python3 """ issue_new.py — create an issue in the local store. Offline, always. The issue is real the moment this writes the file. Nothing is pending, nothing is a draft awaiting a tracker: `origin: local` is a durable state, and pushing it to Gitea later (see /tea:sync) is optional and additive. issue_new.py --type task --title "Wire sqlc into the appclick repo layer" \ --label tech/sql --label comp/appclick issue_new.py --type bug --title "Fix tea-guard crash on empty settings" \ --depends wire-sqlc-appclick --milestone v0.2 Writes tmp/issues/.md prefilled with the type's template, prints the path, and rebuilds INDEX.md. Fill the sections in an editor or with Edit; run issue_check.py when done. Body prose is Russian, section headers and the title are English — see ../references/format.md. """ import argparse import os import sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import issue # noqa: E402 import issue_index # noqa: E402 SPEC = """## Spec none """ TEMPLATES = { "bug": """## Summary Что сломано и где проявляется, одно-два предложения. """ + SPEC + """ ## Steps to reproduce 1. … 2. … ## Expected Что должно было произойти. ## Actual Что происходит на самом деле: вывод команды, лог. ## Environment Только релевантное: версии, ОС, конфигурация. ## Acceptance criteria - [ ] баг не воспроизводится по шагам выше - [ ] добавлена проверка на регрессию (если применимо) """, "task": """## Summary Что нужно сделать, одно-два предложения. """ + SPEC + """ ## Motivation Какую проблему пользователя/системы это решает. ## Acceptance criteria - [ ] проверяемое условие - [ ] … """, "refactor": """## Summary Что перестраиваем и в каких файлах (`path/file:line`). """ + SPEC + """ ## Motivation Чем плохо текущее состояние: дублирование, связность, читаемость. ## Invariants Что НЕ должно измениться: поведение, публичные API, форматы данных. ## Acceptance criteria - [ ] проверяемое условие (тесты зелёные, старый путь удалён, …) """, "test": """## Summary Что покрываем тестами и где (`path/file:line`). """ + SPEC + """ ## Motivation Зачем: регрессия после бага, пробел в покрытии, флаки-тест. ## Test cases - сценарий → ожидаемый результат - … ## Acceptance criteria - [ ] перечисленные кейсы покрыты и зелёные - [ ] тесты проходят в CI """, "feature": """## Summary Бизнес-ценность одним-двумя предложениями. """ + SPEC + """ ## Motivation Какую проблему пользователя/системы это решает. ## Issues - [ ] slug-дочернего-issue — краткое описание части - [ ] … ## Acceptance criteria - [ ] все дочерние issues закрыты - [ ] проверяемое условие уровня фичи """, "draft": """## Summary Идея одним-двумя предложениями. """ + SPEC + """ ## Notes Свободные заметки: что известно, открытые вопросы, варианты. """, } DEPENDS_BLOCK = """## Depends on %s """ def with_depends(body, depends): """Insert `## Depends on` right after `## Spec`, per the format.""" if not depends: return body block = DEPENDS_BLOCK % "\n".join("- %s" % d for d in depends) lines, out, placed = body.splitlines(True), [], False for line in lines: if not placed and line.startswith("## ") and not line.startswith("## Summary") \ and not line.startswith("## Spec") and out: out.append(block + "\n") placed = True out.append(line) if not placed: out.append("\n" + block) return "".join(out) def main(): ap = argparse.ArgumentParser(description="Create a local issue from its type template") ap.add_argument("--type", required=True, choices=sorted(issue.TYPES), help="issue type (becomes the exclusive type/* label)") ap.add_argument("--title", required=True, help="English, imperative, no type prefix") ap.add_argument("--id", help="slug (default: derived from the title)") ap.add_argument("--label", action="append", default=[], help="extra label, e.g. tech/sql; repeat") ap.add_argument("--severity", choices=list(issue.SEVERITIES), help="severity/* label") ap.add_argument("--milestone", default="", help="milestone title") ap.add_argument("--assignee", action="append", default=[], help="assignee; repeat") ap.add_argument("--depends", action="append", default=[], help="id this issue depends on; repeat") ap.add_argument("--out", default=issue.ISSUE_ROOT, help="store root (default: tmp/issues)") args = ap.parse_args() labels = ["type/%s" % args.type] if args.severity: labels.append("severity/%s" % args.severity) labels += [l for l in args.label if l not in labels] id = args.id or issue.unique_id(args.out, issue.slugify(args.title)) if args.id and not issue.SLUG_OK.match(args.id): sys.exit("issue_new.py: --id %r is not a slug (lowercase, digits, single dashes)" % args.id) if os.path.exists(issue.path_of(args.out, id)): sys.exit("issue_new.py: %s already exists" % issue.path_of(args.out, id)) known = set(issue.all_ids(args.out)) for d in args.depends: if d not in known: sys.stderr.write("warning: depends on %r, which is not in the store yet\n" % d) iss = issue.Issue( id=id, title=args.title, body=with_depends(TEMPLATES[args.type], args.depends), labels=labels, assignees=args.assignee, milestone=args.milestone, depends=args.depends) path = issue.save(args.out, iss) issue_index.build(args.out) print("%s [type/%s] %s" % (path, args.type, args.title)) print("fill the sections, then: issue_check.py %s" % id) if __name__ == "__main__": main()