docs(solutions): document Gitea PR-creation workflow (gh/tea limits -> REST API)
This commit is contained in:
@@ -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.
|
||||||
Reference in New Issue
Block a user