diff --git a/docs/solutions/tooling-decisions/gitea-pull-requests.md b/docs/solutions/tooling-decisions/gitea-pull-requests.md new file mode 100644 index 0000000..cf300a4 --- /dev/null +++ b/docs/solutions/tooling-decisions/gitea-pull-requests.md @@ -0,0 +1,71 @@ +--- +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.