Skip to content

Contributing

  1. Put it in tools/ if it is generic, profiles/<codename>/tools/ if it encodes a vendor protocol or one silicon block. Scope it honestly — over-claiming portability is worse than scoping narrowly.

  2. Give it the four-field header. All four are required and enforced:

    #!/bin/bash
    # SPDX-License-Identifier: MIT
    # scope: generic | soc:<soc> | device:<codename>
    # needs: BOOTED | FASTBOOT | FROZEN | INITRAMFS | on-device | any | -
    # env: PHONE, TK_AGENT, ... (or `-`)
    # exits: 0 ok · 1 failed · 75 lock · 76 wrong state
    # One line saying what it does.

    porthole tools lint lists any gaps; make test fails on them.

  3. Never hardcode an IP, username, slot letter or package name. Shell: . tools/ph-lib.sh. Python: import porthole.

  4. If it deliberately induces a reset, put a timeout on every ssh — otherwise it wedges the device lock for everyone else.

  5. Non-trivial logic leaves one runnable check behind.

One file. lib/porthole_cmd_<name>.py exporting a SPEC dict — see docs/ARCHITECTURE.md for the shape, and any existing porthole_cmd_*.py for a worked example. It is discovered at startup; there is no list to update, and shell completion is generated from it automatically.

Terminal window
porthole new-device <codename>

Fill in device.env as you learn. Every blank is a question you now know to ask; checklist.md is the order to answer them in.

If the framework got something wrong for your device — a key that does not fit, an assumption that does not hold — that is the most valuable bug report this project can get. It has only ever been proven against one device.

Adding to the brain

Section: Adding to the brain

A finding is not a trap. A trap warns about territory (“do not do X”); a finding closes a question (“X is already answered”). Findings live in brain/findings/, carry a refutes: line naming the theories they kill, and rank above everything else in search – because an answer that exists outranks a warning about the ground around it.

Terminal window
porthole brain new <id> --severity finding --refutes "the theory it kills"

A note earns its place if it would have saved someone a session.

  • One idea per note. If the title needs an “and”, it is two notes.
  • Cite the evidence. A trap without a source is folklore.
  • Describe the symptom, not just the cause. People search by symptom.
  • Prefer scope: generic, honestly. If it only applied to one device, scope it there.
  • Link with [[note-id]].
  • porthole brain reindex afterwards.

The site is generated, never hand-edited:

Terminal window
cd site-app && npm ci # install the locked static-site toolchain once
cd .. && make docs # stage generated Markdown for Starlight
npm run build --prefix site-app
make docs-serve # preview at http://127.0.0.1:8000/porthole/

Its CLI reference comes from the command registry, its tool catalogue from the tool headers, its profile keys from profiles/_template/device.env, and its knowledge base from brain/. Edit those, not the site.

site-src/, generated content, and site-app/dist/ are gitignored. A committed copy would silently shadow the generated one and let the published docs drift from the code.

Publishing is opt-in. CI builds the documentation on every push and pull request, but deploys to GitHub Pages only when a repository variable says to:

Settings → Secrets and variables → Actions → Variables → PUBLISH_DOCS = true

That default exists because GitHub Pages on a private repository needs a plan that includes it. Attempting to deploy without one puts a permanent red cross on a workflow that is otherwise doing its job, which trains people to ignore CI.

All of these run with no device attached:

Terminal window
make ci # every job GitHub runs, plus the python floor
make check # lint + tests on your interpreter

make ci is CI: every job in .github/workflows/ci.yml runs one of these targets and nothing else, and tests/test_tools.py fails if a job ever grows its own copy of the steps or names a target make ci does not reach. Green here is green on GitHub. The individual jobs, if you want one on its own:

Terminal window
make test # suites, brain lint, shell lib, device mutex
make lint # shellcheck + python syntax
make smoke # fresh clone, bare PATH, empty HOME -- tests/ci-local.sh
make floor # the suite on python 3.8 in a container (needs podman)

Run make ci before you claim something passes. make check compiles with your interpreter, and if yours is newer than the declared floor it will accept syntax CI rejects — a multi-line expression inside an f-string is PEP 701, legal on 3.12+ and a syntax error below it. That exact thing compiled clean locally on 3.14 and broke every CI job.

The floor is declared in three places and a test asserts they agree: bin/porthole, PY_FLOOR in the Makefile, and the CI matrix.

Terminal window
python3 tests/test_config.py # config resolution, legacy aliases
bash tests/test_shell_lib.sh # the same, plus shell/python agreement
python3 tests/test_cli.py # CLI verbs, JSON, exit codes
bash tools/ph-device-test.sh # the device mutex
porthole doctor --tools # every tool has a header

The legacy-alias tests in the first two are the ones to be careful with. Each row corresponds to a command line printed in real documentation; breaking one breaks somebody’s muscle memory silently.

Author and committer are the human. One logical change per commit, and the body explains why.

Contributions written with or without AI are both welcome. Two rules:

  • If you are contributing from a fork, sign off every commit (git commit -s, or git rebase --signoff <base>). The sign-off is your Developer Certificate of Origin: you are certifying you have the right to give us this code. CI fails a fork’s pull request commit whose author has no matching Signed-off-by:. An AI assistant never adds one.

    On a branch of this repository it is not required, and CI does not ask. Only someone with write access can open one, so the certificate would be us asking ourselves about our own work. What certifies those is a maintainer reading the diff and merging, recorded by GitHub — a person looking at the diff, which is the thing a trailer never was. Patches we send upstream still carry a real sign-off from their human author at submission time; porthole aports enforces that one separately.

  • If an AI assistant helped, say so with an Assisted-by: trailer (for example Assisted-by: Claude), or Generated-by: when it wrote nearly all of it. Nothing requires the trailer: a commit written without AI needs nothing. AI.md, at the top of the repository, says how this project itself uses AI.

Banned on commits, pull request bodies and issue bodies alike, because it attributes the work wrongly: a Co-Authored-By:, Co-developed-by: or Signed-off-by: naming an AI, a Claude-Session: line, a session URL, a generated-with line. lib/porthole_trailers.py holds the one pattern list; the commit hook rejects a message with it (it never rewrites one), and the Commit check CI job fails a pull request whose body or log matches it. make trailers runs the log check locally, and python3 lib/porthole_trailers.py --dco origin/main..HEAD checks a range’s sign-offs by hand — what you want before sending a series upstream.

A cherry-picked commit keeps its original author — git cherry-pick -x. On a community port a lot of the early device tree is someone else’s work, and getting this wrong is both rude and a licensing problem.

Reviewing and merging

Section: Reviewing and merging

Most of the work here is opened by an assistant, so the review is where a human enters the loop — there is no trailer standing in for one, and no script to run before merging. Reviewing the diff and pressing merge IS this project’s provenance record.

Three commands, and none of them needs a clone:

Terminal window
gh pr diff <N> # the whole diff, in the terminal
gh pr diff <N> --patch | git apply --check # does it still apply cleanly?
gh pr merge <N> --squash --delete-branch # when you are happy

To review commit by commit rather than as one diff — which is the point of keeping them separate — open the pull request’s Commits tab and click each one. GitHub shows that commit alone, with its own message and its own comment threads. gh pr view <N> --web opens it.

A pull request body here states which claims were verified by execution and which were only read (state-what-you-verified). Read that first: it tells you which parts of the diff have evidence behind them and which are the author’s belief, and those are the parts worth your attention.

The branch protection

Section: The branch protection

.github/main-ruleset.json is the ruleset main carries: a pull request is required, CI passed must be green, and force-pushes and deletion are blocked. Organization admins bypass it, so a hotfix is never locked out.

It deliberately requires zero approving reviews. GitHub will not let you approve a pull request you opened, so a required-review rule would block every branch a solo maintainer pushes — a gate that cannot be satisfied is the thing this project just finished removing. The merge button is the gate; the ruleset is there to stop an accident, not to stand in for reading the diff.

Apply or update it with:

Terminal window
gh api -X POST repos/porthole-dev/<repo>/rulesets \
--input .github/main-ruleset.json # first time
gh api -X PUT repos/porthole-dev/<repo>/rulesets/<id> \
--input .github/main-ruleset.json # to change it later
gh api repos/porthole-dev/<repo>/rulesets # what is in force now

The organization is on the free plan, which has no organization-wide rulesets, so this is applied per repository.