4.3 KiB
4.3 KiB
title, date, category, module, problem_type, component, severity, applies_when, tags
| title | date | category | module | problem_type | component | severity | applies_when | tags | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Creating pull requests on the self-hosted Gitea remote | 2026-06-27 | tooling-decisions | dev-workflow / gitea | tooling_decision | development_workflow | low |
|
|
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(ownerm038, repoimmich-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.
- 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.) - Read
GITEA_HOST,GITEA_USER,GITEA_TOKENfrom.env(awrite:repository-scoped token). Never print, echo, or commit.env—sourceit; keep the token out ofargv(use acurl -Kconfig file, mode 0600) and shred the file after. POST {base}/api/v1/repos/{GITEA_USER}/{repo}/pullswith{head, base, title, body}.
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
ghis GitHub-only. It errorsnone of the git remotes ... point to a known GitHub hostand 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 withssh_agent: trueand the key already loaded in the agent. From an agent/CI shell there is no TTY, so it dies withcould not open TTY. (Plaingit-over-SSH still works because the agent answers the key; onlytea's own auth flow needs the TTY.) The user can runteain 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
mastergave the PR a base, and a laterPATCH default_branchfixed 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-flowand~/Nextcloud/Projects/immich-photo-floware the same repo (~/Projectsis a symlink), not two clones.