--- title: Creating pull requests on the self-hosted Gitea remote date: 2026-06-27 category: tooling-decisions module: dev-workflow / gitea problem_type: tooling_decision component: development_workflow severity: low applies_when: - Opening or editing a PR for this repo from a non-interactive agent/CI shell - "`gh pr create` fails with: none of the git remotes point to a known GitHub host" - tea exits with "Failed to read SSH passphrase ... could not open TTY" tags: [gitea, pull-request, tea, gh, rest-api, ssh, dotenv] --- # Creating pull requests on the self-hosted Gitea remote ## Context This repo's `origin` is a **self-hosted Gitea** instance, not GitHub: - API/web base: `https://git.gorinskat.nl` (owner `m038`, repo `immich-photo-flow`) - Push remote (SSH): `ssh://git@m038-nas.tail63ee39.ts.net:222/m038/immich-photo-flow.git` — a *different host* than the API, reverse-proxied behind nginx. Opening a PR from an agent/non-interactive shell fails through the two obvious tools, which wastes a lot of back-and-forth if you don't know why. ## Guidance Open PRs via the **Gitea REST API over HTTPS**, authenticated with a token from `.env`. Do not rely on `gh` or `tea` from a non-interactive shell. 1. Make sure the **base branch already exists on the server** — a PR needs it. (Gitea sets the repo default branch to the *first* branch you push, so push your base branch, e.g. `main`/`master`, before or alongside the feature branch.) 2. Read `GITEA_HOST`, `GITEA_USER`, `GITEA_TOKEN` from `.env` (a `write:repository`-scoped token). **Never print, echo, or commit `.env`** — `source` it; keep the token out of `argv` (use a `curl -K` config file, mode 0600) and shred the file after. 3. `POST {base}/api/v1/repos/{GITEA_USER}/{repo}/pulls` with `{head, base, title, body}`. ```bash set -a; . .env; set +a # never cat/echo this file case "$GITEA_HOST" in *://*) base="$GITEA_HOST";; *) base="https://$GITEA_HOST";; esac umask 077; cfg=$(mktemp); printf 'header = "Authorization: token %s"\n' "$GITEA_TOKEN" > "$cfg" # body via file -> JSON payload (avoids backtick/$ re-evaluation in the body) python3 - <<'PY' import json; json.dump({"head":"feat/my-branch","base":"main", "title":"feat: ...","body":open("/path/to/body.md").read()}, open("/tmp/pr.json","w")) PY curl -sS -K "$cfg" -X POST -H "Content-Type: application/json" --data @/tmp/pr.json \ "${base%/}/api/v1/repos/$GITEA_USER/$repo/pulls" -w '\nHTTP %{http_code}\n' shred -u "$cfg" ``` Repo-level settings use the same API, e.g. set the default branch: `PATCH {base}/api/v1/repos/{owner}/{repo}` with `{"default_branch":"main"}` (only do this with explicit user consent — it's a persistent change to shared infra). ## Why This Matters - **`gh` is GitHub-only.** It errors `none of the git remotes ... point to a known GitHub host` and cannot target a Gitea instance. - **`tea` (0.14) can't authenticate non-interactively here.** Its login is configured with an SSH key that has a passphrase, and it insists on reading the passphrase from `/dev/tty` — even with `ssh_agent: true` and the key already loaded in the agent. From an agent/CI shell there is no TTY, so it dies with `could not open TTY`. (Plain `git`-over-SSH still works because the agent answers the key; only `tea`'s own auth flow needs the TTY.) The user *can* run `tea` in their own terminal — it only fails for non-interactive callers. - The REST API needs neither GitHub nor a TTY, so it's the reliable path for automation. ## When to Apply - Any time an agent needs to open or edit a PR (or change repo settings) on this Gitea remote. - Generalizes to any non-GitHub Gitea/Forgejo remote reached from a non-interactive shell. ## Examples - **Symptom → cause:** `gh pr create` → wrong forge; `tea pulls create` → `Failed to read SSH passphrase: could not open TTY` → use the REST API instead. - **Gotcha:** after pushing only a feature branch first, the repo default branch became the feature branch; pushing `master` gave the PR a base, and a later `PATCH default_branch` fixed the default. ## Related - Credentials live in `.env` (gitignored): `GITEA_HOST`, `GITEA_USER`, `GITEA_TOKEN` — documented in `.env.example`. - The two repo paths `~/Projects/immich-photo-flow` and `~/Nextcloud/Projects/immich-photo-flow` are the same repo (`~/Projects` is a symlink), not two clones.