CI on every PR, issue forms, PR template, code of conduct, examples validator

The workflow builds the launcher with its unit tests and a 400 KB size guard, and runs the
tools tests plus a new tests/test_examples.py that checks every list's format and that both
TV tables index every folder. Issue forms for TV reports and bugs, a PR checklist from
CONTRIBUTING, and a short plain-language code of conduct.
This commit is contained in:
Gathin23 committed 2026-10-19 23:20:21 +05:30
1 parent 1636b9ec42
commit 97a1b24cb2
7 files changed
+291

No files matched your search

+43
View File
@@ -0,0 +1,43 @@
name: Bug
description: The launcher, a script or the prompt did something wrong
labels: [bug]
body:
- type: dropdown
id: part
attributes:
label: Which part
options:
- Launcher (the home screen)
- Install script (build-and-install)
- Debloat or restore scripts
- tools/tv.py or b64push.py
- The prompt (PROMPT.md)
- Website (cleanyour.tv)
validations:
required: true
- type: input
id: tv
attributes:
label: TV model and Android version
placeholder: Xiaomi MiTV-MOOQ1, Android TV 10
validations:
required: true
- type: textarea
id: what
attributes:
label: What happened, and what you expected
validations:
required: true
- type: textarea
id: steps
attributes:
label: Steps to reproduce
placeholder: |
1. Press Down twice from the dock
2. Press Home
- type: textarea
id: logs
attributes:
label: Output or screenshot
description: Script output, a `logcat` excerpt, or a screenshot pulled from the TV.
render: shell
+11
View File
@@ -0,0 +1,11 @@
blank_issues_enabled: true
contact_links:
- name: Question or "does it work on my TV?"
url: https://github.com/Gathin23/cleanyourtv/discussions/categories/q-a
about: Ask in Discussions. Issues are for bugs and TV lists.
- name: Show what you built
url: https://github.com/Gathin23/cleanyourtv/discussions/categories/show-and-tell
about: Screenshots, themes, forks. Launchers and ports also go in SHOWCASE.md by pull request.
- name: cleanyour.tv
url: https://cleanyour.tv
about: The site, with the prompt and the story.
+48
View File
@@ -0,0 +1,48 @@
name: My TV
description: A TV that is not in examples/ yet, or a list that needs a change
title: "[TV] Brand Model (Android version)"
labels: [tv-list]
body:
- type: markdown
attributes:
value: |
Lists people have tested live in [`examples/`](../tree/main/examples). If you already have a working list, a pull request is even better: see [CONTRIBUTING.md](../blob/main/CONTRIBUTING.md).
- type: input
id: model
attributes:
label: TV model
description: "`tools/tv.py --probe` prints the model, device codename, Android version and build."
placeholder: Sony BRAVIA KD-55X8509C (SVP-DTV15)
validations:
required: true
- type: input
id: android
attributes:
label: Android version and firmware build
placeholder: Android TV 11, build RTT1.230922.001
validations:
required: true
- type: dropdown
id: stage
attributes:
label: How far did you get?
options:
- Could not connect over ADB
- Debloat worked, launcher did not
- Both worked, here is my list
- Something broke after the debloat
validations:
required: true
- type: textarea
id: details
attributes:
label: What happened
description: What you ran, what the TV did, what you expected. Paste the packages you disabled if you have them.
render: shell
validations:
required: true
- type: textarea
id: screenshot
attributes:
label: Screenshot
description: "Optional. `tools/tv.py 'screencap -p /sdcard/s.png'` then `tools/tv.py --pull /sdcard/s.png s.png`, and drop the file here."
+16
View File
@@ -0,0 +1,16 @@
<!-- Thanks. One or two sentences on what this changes and the TV it was tried on is plenty. -->
**Tested on:** <!-- model, Android version -->
### If this adds or changes a TV list
- [ ] `examples/<brand>-<model>/debloat.txt`, header with model, Android version and firmware build
- [ ] Only packages I actually disabled and lived with; anything I had to re-enable is noted
- [ ] A row in `examples/README.md` and one in the README's TV table
- [ ] Nothing in the list uninstalls
### If this touches the launcher
- [ ] Verified on a real TV, screenshot attached
- [ ] D-pad checked from every edge (Up, Down, Left, Right)
- [ ] No network, no background service, no new permission
- [ ] New strings in `values/strings.xml` and the other `values-*` folders
- [ ] Anything TV-specific is in `config.xml` or a theme, not in code
+49
View File
@@ -0,0 +1,49 @@
name: CI
on:
push:
branches: [main]
pull_request:
permissions:
contents: read
jobs:
lists-and-tools:
name: Lists, tools and docs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Tools virtualenv
run: |
python3 -m venv tools/venv
tools/venv/bin/pip install -q -r tools/requirements.txt
- name: Example lists, README tables, shell helpers
run: python3 -m unittest discover -s tests -v
launcher:
name: Launcher build and unit tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "17"
- uses: android-actions/setup-android@v3
with:
packages: platforms;android-34 build-tools;34.0.0
- uses: gradle/actions/setup-gradle@v4
- name: Unit tests and release build
working-directory: launcher
run: |
chmod +x gradlew
./gradlew --no-daemon testReleaseUnitTest assembleRelease
- name: The launcher stays small
run: |
size=$(stat -c %s launcher/app/build/outputs/apk/release/app-release.apk)
echo "release APK: $((size / 1024)) KB"
test "$size" -lt 409600 || { echo "APK is over 400 KB; the launcher targets 1 GB TVs, keep it small"; exit 1; }
+36
View File
@@ -0,0 +1,36 @@
# Code of conduct
This project exists because a few thousand people helped each other fix their TVs in the replies
to a tweet. Keep it like that.
## Expected
- Be kind. Many contributors are doing this on a parent's or grandparent's TV, in a second
language, on their first pull request.
- Assume good faith. A list that breaks something was tested on different firmware, not written to
hurt anyone.
- Say what you tested on. Specifics ("Android TV 11, build RTT1, Hisense A4") help; verdicts
("doesn't work") do not.
- Respect the maintainers' decisions on scope. The launcher stays small, offline and without
prebuilt binaries; if your idea does not fit, a fork with a SHOWCASE row is a good outcome, not a
rejection of you.
## Not accepted
- Insults, harassment, slurs, sexualised language or imagery, or personal attacks, in issues,
pull requests, Discussions or the showcase.
- Publishing other people's private information: device identifiers, Wi-Fi names, IP addresses,
ADB keys or anything that could identify a TV or a home.
- Lists or instructions that uninstall, root, or intentionally damage a TV, or that target a
specific person's device.
- Spam, affiliate links, or passing off someone else's work as your own.
## Enforcement
Maintainers may edit, hide or remove content and block accounts that break these rules, with a
short explanation. To report something, open an issue, or if it is private, message
[@gathint on X](https://x.com/gathint). Reports are read by the maintainer and handled within a
few days.
This is a short, plain-language code adapted in spirit from the
[Contributor Covenant](https://www.contributor-covenant.org/version/2/1/code_of_conduct/).
+88
View File
@@ -0,0 +1,88 @@
"""Checks every TV list in examples/ and the two tables that index them.
Run from the repository root: python3 -m unittest discover -s tests -v
"""
import re
from pathlib import Path
import unittest
ROOT = Path(__file__).resolve().parents[1]
EXAMPLES = ROOT / "examples"
PACKAGE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z0-9_]+)+$")
def folders():
return sorted(p for p in EXAMPLES.iterdir() if p.is_dir())
def table_rows(path, header_prefix):
"""Rows of the first markdown table whose header starts with header_prefix."""
lines = path.read_text(encoding="utf-8").split("\n")
start = next(i for i, l in enumerate(lines) if l.startswith(header_prefix))
rows = []
for line in lines[start + 2:]:
if not line.startswith("|"):
break
rows.append(line)
return rows
class ExampleListsTest(unittest.TestCase):
def test_every_folder_has_a_list(self):
for folder in folders():
with self.subTest(folder=folder.name):
self.assertTrue((folder / "debloat.txt").is_file(), "no debloat.txt")
def test_list_format(self):
for folder in folders():
path = folder / "debloat.txt"
if not path.is_file():
continue
with self.subTest(folder=folder.name):
lines = path.read_text(encoding="utf-8").split("\n")
self.assertTrue(lines and lines[0].startswith("#"),
"first line should be a comment naming the TV, Android version and build")
packages = []
for n, line in enumerate(lines, 1):
body = line.split("#", 1)[0].strip()
if not body:
continue
self.assertRegex(body, PACKAGE, f"line {n}: not a package name: {line!r}")
packages.append(body)
self.assertTrue(packages, "no packages: notes without a list belong in docs/devices/")
dupes = sorted({p for p in packages if packages.count(p) > 1})
self.assertFalse(dupes, f"listed twice: {dupes}")
text = path.read_text(encoding="utf-8")
self.assertNotIn("pm uninstall", text, "lists disable, they never uninstall")
def test_examples_table_matches_folders(self):
rows = table_rows(EXAMPLES / "README.md", "| Folder |")
listed = []
for row in rows:
m = re.match(r"\|\s*`([^`/]+)/`\s*\|", row)
self.assertIsNotNone(m, f"row does not start with a `folder/` cell: {row[:60]}")
listed.append(m.group(1))
names = [f.name for f in folders()]
self.assertEqual(sorted(listed), sorted(names),
"examples/README.md rows and examples/ folders differ")
self.assertEqual(len(listed), len(set(listed)), "a folder is listed twice")
def test_readme_table_has_one_row_per_list(self):
readme_rows = table_rows(ROOT / "README.md", "| TV | OS |")
example_rows = table_rows(EXAMPLES / "README.md", "| Folder |")
self.assertEqual(len(readme_rows), len(example_rows),
"README.md TV table and examples/README.md have a different number of rows")
readme_tvs = sorted(r.split("|")[1].strip() for r in readme_rows)
example_tvs = sorted(r.split("|")[2].strip() for r in example_rows)
self.assertEqual(readme_tvs, example_tvs, "the TV column differs between the two tables")
def test_readme_counts(self):
text = (ROOT / "README.md").read_text(encoding="utf-8")
n = len(folders())
self.assertIn(f"{n} TV lists", text, "nav link count is stale")
self.assertIn(f"{n} TVs with tested lists", text, "numbers line is stale")
if __name__ == "__main__":
unittest.main()