feat(feedback-bundle): commit-linked Gitea tickets with file attachments

New model-invoked skill + feedback_bundle.py: creates a Gitea issue whose
body links the current HEAD commit and uploads logs/screenshots as issue
assets. Handles label-name-to-ID resolution, tea-config token lookup, and
the disabled-attachments (403/404) case. Index updated in root and
engineering READMEs.
This commit is contained in:
2026-10-06 14:06:01 -04:00
parent 01c488157d
commit 71525bc349
4 changed files with 226 additions and 0 deletions
+2
View File
@@ -7,6 +7,7 @@ Skills are grouped by invocation type. [User-invoked](docs/invocation.md) skills
## Credits ## Credits
Many of these skills were created or originated by the following people and organizations: Many of these skills were created or originated by the following people and organizations:
- [Matt Pocock](http://mattpocock.com) - [Matt Pocock](http://mattpocock.com)
- [poteto](https://github.com/poteto?tab=repositories) - [poteto](https://github.com/poteto?tab=repositories)
@@ -84,6 +85,7 @@ Many of these skills were created or originated by the following people and orga
### Engineering ### Engineering
- [feedback-bundle](skills/engineering/feedback-bundle/SKILL.md) — Post a Gitea feedback issue linked to the current commit and attach logs or screenshots. Use when the user reports a problem and asks to file it with evidence, open a bug ticket, or says “feedback-bundle” or “attach the log.”
- [code-review](skills/engineering/code-review/SKILL.md) — Review the changes since a fixed point (commit, branch, tag, or merge-base) along two axes: Standards (does the code follow this repo's documented coding standards?) and Spec (does the code match what the originating issue/spec asked for?). Runs both reviews in parallel sub-agents and reports them side by side. Use when the user wants to review a branch, a PR, work-in-progress changes, or asks to "review since X". - [code-review](skills/engineering/code-review/SKILL.md) — Review the changes since a fixed point (commit, branch, tag, or merge-base) along two axes: Standards (does the code follow this repo's documented coding standards?) and Spec (does the code match what the originating issue/spec asked for?). Runs both reviews in parallel sub-agents and reports them side by side. Use when the user wants to review a branch, a PR, work-in-progress changes, or asks to "review since X".
- [codebase-design](skills/engineering/codebase-design/SKILL.md) — Shared vocabulary for designing deep modules. Use when the user wants to design or improve a module's interface, find deepening opportunities, decide where a seam goes, make code more testable or AI-navigable, or when another skill needs the deep-module vocabulary. - [codebase-design](skills/engineering/codebase-design/SKILL.md) — Shared vocabulary for designing deep modules. Use when the user wants to design or improve a module's interface, find deepening opportunities, decide where a seam goes, make code more testable or AI-navigable, or when another skill needs the deep-module vocabulary.
- [diagnosing-bugs](skills/engineering/diagnosing-bugs/SKILL.md) — Diagnosis loop for hard bugs and performance regressions. Use when the user says "diagnose"/"debug this", or reports something broken/throwing/failing/slow. - [diagnosing-bugs](skills/engineering/diagnosing-bugs/SKILL.md) — Diagnosis loop for hard bugs and performance regressions. Use when the user says "diagnose"/"debug this", or reports something broken/throwing/failing/slow.
+1
View File
@@ -22,6 +22,7 @@ Daily code work.
## Model-invoked ## Model-invoked
- [feedback-bundle](feedback-bundle/SKILL.md) — Post a Gitea feedback issue linked to the current commit and attach logs or screenshots. Use when the user reports a problem and asks to file it with evidence, open a bug ticket, or says “feedback-bundle” or “attach the log.”
- [code-review](code-review/SKILL.md) — Review the changes since a fixed point (commit, branch, tag, or merge-base) along two axes: Standards (does the code follow this repo's documented coding standards?) and Spec (does the code match what the originating issue/spec asked for?). Runs both reviews in parallel sub-agents and reports them side by side. Use when the user wants to review a branch, a PR, work-in-progress changes, or asks to "review since X". - [code-review](code-review/SKILL.md) — Review the changes since a fixed point (commit, branch, tag, or merge-base) along two axes: Standards (does the code follow this repo's documented coding standards?) and Spec (does the code match what the originating issue/spec asked for?). Runs both reviews in parallel sub-agents and reports them side by side. Use when the user wants to review a branch, a PR, work-in-progress changes, or asks to "review since X".
- [codebase-design](codebase-design/SKILL.md) — Shared vocabulary for designing deep modules. Use when the user wants to design or improve a module's interface, find deepening opportunities, decide where a seam goes, make code more testable or AI-navigable, or when another skill needs the deep-module vocabulary. - [codebase-design](codebase-design/SKILL.md) — Shared vocabulary for designing deep modules. Use when the user wants to design or improve a module's interface, find deepening opportunities, decide where a seam goes, make code more testable or AI-navigable, or when another skill needs the deep-module vocabulary.
- [diagnosing-bugs](diagnosing-bugs/SKILL.md) — Diagnosis loop for hard bugs and performance regressions. Use when the user says "diagnose"/"debug this", or reports something broken/throwing/failing/slow. - [diagnosing-bugs](diagnosing-bugs/SKILL.md) — Diagnosis loop for hard bugs and performance regressions. Use when the user says "diagnose"/"debug this", or reports something broken/throwing/failing/slow.
@@ -0,0 +1,40 @@
---
name: feedback-bundle
description: Post a Gitea feedback issue linked to the current commit and attach logs or screenshots. Use when the user reports a problem and asks to file it with evidence, open a bug ticket, or says "feedback-bundle" or "attach the log".
---
# Feedback Bundle
Create a Gitea issue in the current repository, linked to its current `HEAD`, with optional files attached.
## Run
Run from the repository the issue should belong to, using `feedback_bundle.py` beside this file:
```sh
python3 <skill-dir>/feedback_bundle.py "Describe the problem" @app.log @screenshot.png
```
The script requires Python `requests`. Without `--token`, it reads the matching host token from `~/.config/tea/config.yml` and requires PyYAML. The default repository is read from `git remote get-url origin`.
Common options:
```sh
python3 <skill-dir>/feedback_bundle.py "Description" @log.txt \
--title "Short issue title" --label bug --dry-run
```
- `@file` — attach an existing file; paths are resolved from the current working directory. The `@` is optional.
- `--title` — override the default title (the description's first line).
- `--label` — add a label; repeat to add more. Labels are resolved against the repository's Gitea labels; unknown labels are skipped with a warning.
- `--repo` — override the remote URL. Use a full Git remote URL (SSH, SCP-style, or HTTPS), not an `owner/repo` slug.
- `--token` — provide a Gitea token instead of reading tea config.
- `--dry-run` — print the issue preview without creating an issue or uploading files. Authentication is still loaded; labels require an API request.
## What happens
1. The script reads `HEAD`, repository host/owner/name, and authentication.
2. It creates an issue whose body includes a link to that exact commit.
3. It uploads each attachment to the issue and adds download links to the body.
Gitea only accepts issue attachments after an issue exists. If uploads fail because attachments are disabled (403/404), the issue already exists without the files; enable attachments on the Gitea instance, then attach the files to that issue manually. The instance setting is `[attachment] ENABLED = true` in `app.ini` (or `ATTACHMENTS_ENABLED`), not a web UI toggle.
@@ -0,0 +1,183 @@
#!/usr/bin/env python3
"""Post a feedback ticket to a Gitea repo, linked to the current commit, with
files attached to the issue.
feedback_bundle.py [--title T] [--label L ...] [--repo REMOTE_URL] [--token T]
[--dry-run] <description> [@file ...]
Flow (Gitea attaches files to an existing issue, there is no standalone upload
endpoint): create the issue, then POST each file to
/repos/{owner}/{repo}/issues/{number}/assets (multipart field "attachment").
The ticket body links to `git HEAD` and lists the attached files.
If the assets endpoint returns 403/404 the instance has attachments disabled
(admin `[attachment] ENABLED = true` in app.ini — not a web-UI setting); the
script reports this instead of posting a ticket with no attachments.
"""
from __future__ import annotations
import argparse
import os
import re
import subprocess
import sys
try:
import requests
except ImportError: # pragma: no cover
sys.exit("requests is required: pip install requests")
def run(*args: str) -> str:
return subprocess.run(args, capture_output=True, text=True, check=True).stdout.strip()
def parse_remote(remote: str | None) -> tuple[str, str, str]:
"""(host, owner, repo) from a git remote URL (scp, ssh, or https forms)."""
url = remote or run("git", "remote", "get-url", "origin")
u = re.sub(r"^[a-z]+://", "", url.strip(), flags=re.I) # strip scheme
u = u.split("@", 1)[-1] # strip user@
u = u.replace(".git", "").rstrip("/")
host, rest = (u.split(":", 1) if ":" in u else u.split("/", 1))
owner, repo = rest.split("/", 1)
return host, owner, repo
def load_token(host: str, token: str | None) -> str:
if token:
return token
cfg = os.path.expanduser("~/.config/tea/config.yml")
if not os.path.exists(cfg):
sys.exit("no --token and no tea config found; cannot authenticate")
import yaml
data = yaml.safe_load(open(cfg).read())
for login in data.get("logins", []):
if login.get("url").rstrip("/").endswith(host) and login.get("token"):
return login["token"]
sys.exit(f"no stored token for host {host}; pass --token")
def resolve_labels(host, token, owner, repo, names):
"""Gitea's issue API wants label IDs, not names. Map names to IDs; warn on
unknown labels and drop them."""
if not names:
return []
url = f"https://{host}/api/v1/repos/{owner}/{repo}/labels"
r = requests.get(url, headers={"Authorization": f"token {token}"})
if r.status_code >= 400:
sys.exit(f"could not list labels ({r.status_code}): {r.text}")
by_name = {l["name"]: l["id"] for l in r.json()}
ids = []
for n in names:
if n in by_name:
ids.append(by_name[n])
else:
print(f"warning: unknown label '{n}', skipping", file=sys.stderr)
return ids
def create_issue(host, token, owner, repo, title, body, label_ids):
url = f"https://{host}/api/v1/repos/{owner}/{repo}/issues"
r = requests.post(url, json={"title": title, "body": body, "labels": label_ids},
headers={"Authorization": f"token {token}"})
if r.status_code >= 400:
sys.exit(f"issue create failed ({r.status_code}): {r.text}")
return r.json()
def attach(host, token, owner, repo, issue_number, path):
"""Attach one file to an issue. Returns the browser_download_url, or None
on 403/404 (attachments disabled on this instance)."""
url = f"https://{host}/api/v1/repos/{owner}/{repo}/issues/{issue_number}/assets"
with open(path, "rb") as f:
r = requests.post(url, files={"attachment": f},
params={"name": os.path.basename(path)},
headers={"Authorization": f"token {token}"})
if r.status_code in (403, 404):
return None
r.raise_for_status()
return r.json().get("browser_download_url")
def main() -> int:
ap = argparse.ArgumentParser(description="Post a commit-linked feedback ticket with attachments.")
ap.add_argument("description", help="ticket description (text before any @file)")
ap.add_argument("files", nargs="*", help="files to attach (@name or name)")
ap.add_argument("--title", help="ticket title (default: first line of description)")
ap.add_argument("--label", action="append", default=[], help="label to add (repeatable)")
ap.add_argument("--repo", help="Git remote URL (default: git remote origin)")
ap.add_argument("--token", help="Gitea token (default: tea config for the host)")
ap.add_argument("--dry-run", action="store_true", help="print the ticket without posting it")
args = ap.parse_args()
files = [f.lstrip("@") for f in args.files]
for p in files:
if not os.path.isfile(p):
sys.exit(f"not a file: {p}")
host, owner, repo = parse_remote(args.repo) if args.repo else (None, None, None)
if not host:
host, owner, repo = parse_remote(None)
token = load_token(host, args.token)
sha = run("git", "rev-parse", "HEAD")
short = sha[:7]
commit_url = f"https://{host}/{owner}/{repo}/commit/{sha}"
desc = args.description.strip()
title = (args.title or desc).strip().splitlines()[0] if desc else "(no description)"
labels = resolve_labels(host, token, owner, repo, [l for l in args.label if l])
seed = f"{desc}\n\n**Linked commit:** [{short}]({commit_url}) (`{sha}`)"
# Preview only: no issue is created and nothing is uploaded.
if args.dry_run:
att_line = "\n".join(f"- {os.path.basename(p)}" for p in files) or "(none)"
body = seed + "\n\n**Attachments:**\n" + att_line
print("=== DRY RUN (nothing created) ===")
print(f"repo: {owner}/{repo}")
print(f"commit: {commit_url}")
print(f"title: {title}")
print(f"labels: {[l for l in args.label if l] or '(none)'}")
print(f"attach: {att_line}")
print("=== body ===")
print(body)
return 0
# Create the issue first (Gitea attaches files to an existing issue).
issue = create_issue(host, token, owner, repo, title, seed, labels)
number = issue["number"]
attached = []
missing = False
for p in files:
url = attach(host, token, owner, repo, number, p)
if url is None:
missing = True
else:
attached.append((os.path.basename(p), url))
if missing and not attached:
print(
"ERROR: attachment endpoint returned 403/404 — attachments are disabled on this "
"instance (admin setting `[attachment] ENABLED = true` in app.ini; there is no "
"web-UI toggle). Enable it and re-run. The issue was created but has no attachments.",
file=sys.stderr,
)
return 2
if attached:
body = seed + "\n\n**Attachments:**\n" + "\n".join(f"- [{n}]({u})" for n, u in attached)
patch_url = f"https://{host}/api/v1/repos/{owner}/{repo}/issues/{number}"
requests.patch(patch_url, json={"body": body},
headers={"Authorization": f"token {token}"})
if missing:
print(f"warning: {sum(1 for _ in range(0)) + (len(files) - len(attached))} file(s) could not be attached (instance may have attachments disabled)",
file=sys.stderr)
print(f"Created: {issue.get('html_url', '')}")
return 0
if __name__ == "__main__":
raise SystemExit(main())