Let pushes go by an HTTPS token or an SSH deploy key
CI / checks (push) Skipped
CI / checks (pull_request) Failing after 56s

The push credential is either a push-only account's token, handed to git
through a credential helper that names environment variables, never
values, or a deploy key in a short-lived ssh-agent. The remote URL
follows the credential. Configured as gitea.push_user and push_token, or
gitea.push_key.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
This commit is contained in:
dongarandClaude Opus 5.5 committed 2026-10-02 21:59:56 +00:00
1 parent 761ebdb857
commit c931ac939b
15 files changed
+241 -81

No files matched your search

+78 -15
View File
@@ -3,9 +3,15 @@
Every command runs on the build VM over SSH. Remotes are plain URLs with no
credential in them, so nothing secret is ever written to the clone's
configuration (#89). Operations that reach the remote (clone, fetch, push,
ls-remote) are handed the push key for that one operation: it is fed over
stdin to a short-lived ``ssh-agent`` on the VM and gone when the command
ends. The model's commands never receive it.
ls-remote) are handed the push credential for that one operation, and the
model's commands never receive it:
- an HTTPS token (the default: HTTPS reaches the forge from everywhere) goes
to git through a credential helper that names environment variables, never
values; the values are that one command's secrets, so they are never on a
command line and are redacted from its output;
- an SSH key is fed over stdin to a short-lived ``ssh-agent`` on the VM, gone
when the command ends.
Pull requests are opened from the agent's own host with its Gitea API token
(publish.py), which never reaches the VM.
@@ -27,6 +33,31 @@ class GitError(RuntimeError):
pass
@dataclass(frozen=True)
class HTTPSToken:
"""A push-only account's token, for HTTPS remotes (D4)."""
username: str
token: str
@dataclass(frozen=True)
class SSHKey:
"""A deploy key's private half, for SSH remotes (D4)."""
private_key: str
PushCredential = HTTPSToken | SSHKey
# git asks the helper for credentials; it answers from the environment, so
# the command line names variables, never values.
_HELPER = (
'!f() { test "$1" = get || return 0; '
'echo "username=$NISSE_PUSH_USER"; echo "password=$NISSE_PUSH_TOKEN"; }; f'
)
def _with_key(command: str, known_hosts: str | None) -> str:
"""Run ``command`` with the key on stdin loaded into a throwaway agent."""
git_ssh = _GIT_SSH
@@ -48,13 +79,33 @@ class GitRepo:
# the VM user's own known_hosts.
known_hosts: str | None = None
def _git(self, *args: str, key: str | None = None, cwd: str | None = None) -> str:
command = "git " + " ".join(shlex.quote(a) for a in args)
def _git(
self,
*args: str,
credential: PushCredential | None = None,
cwd: str | None = None,
) -> str:
quoted = " ".join(shlex.quote(a) for a in args)
command, stdin, secrets = f"git {quoted}", None, {}
if isinstance(credential, SSHKey):
command = _with_key(command, self.known_hosts)
stdin = credential.private_key
elif isinstance(credential, HTTPSToken):
# The empty helper first clears any configured on the VM.
command = (
"GIT_TERMINAL_PROMPT=0 git -c credential.helper= "
f"-c credential.helper={shlex.quote(_HELPER)} {quoted}"
)
secrets = {
"NISSE_PUSH_USER": credential.username,
"NISSE_PUSH_TOKEN": credential.token,
}
try:
result = self.host.execute(
_with_key(command, self.known_hosts) if key else command,
command,
cwd=cwd or str(self.root),
stdin=key,
stdin=stdin,
secrets=secrets,
timeout_s=TIMEOUT_S,
)
except HostUnavailable as exc:
@@ -87,11 +138,13 @@ class GitRepo:
return int(self._git("rev-list", "--count", f"{ref}..HEAD"))
def remote_sha(
self, remote_url: str, branch: str, *, key: str | None = None
self, remote_url: str, branch: str, *, credential: PushCredential | None = None
) -> str | None:
"""What the remote's branch points at, or None if it has no such
branch. Pushes go by URL, so there is no tracking ref to read."""
out = self._git("ls-remote", remote_url, f"refs/heads/{branch}", key=key)
out = self._git(
"ls-remote", remote_url, f"refs/heads/{branch}", credential=credential
)
return out.split()[0] if out else None
def exists(self) -> bool:
@@ -110,12 +163,16 @@ class GitRepo:
self.host.execute(f"mkdir -p -- {shlex.quote(str(self.root))}")
self._git("init", "-q", "-b", default_branch)
def clone(self, remote_url: str, *, key: str | None = None) -> None:
def clone(
self, remote_url: str, *, credential: PushCredential | None = None
) -> None:
"""Clone into the workspace root. The remote URL carries no
credential, so none is stored in the clone's configuration."""
parent = str(self.root.parent)
self.host.execute(f"mkdir -p -- {shlex.quote(parent)}")
self._git("clone", "-q", remote_url, str(self.root), key=key, cwd=parent)
self._git(
"clone", "-q", remote_url, str(self.root), credential=credential, cwd=parent
)
def configure_identity(self, name: str, email: str) -> None:
self._git("config", "user.name", name)
@@ -143,10 +200,14 @@ class GitRepo:
self._git("commit", "-q", "-m", message)
return self.head_sha()
def push(self, remote_url: str, branch: str, *, key: str | None = None) -> None:
def push(
self, remote_url: str, branch: str, *, credential: PushCredential | None = None
) -> None:
# By URL, per invocation, rather than a configured remote with a
# credential in it.
self._git("push", "-q", remote_url, f"HEAD:refs/heads/{branch}", key=key)
self._git(
"push", "-q", remote_url, f"HEAD:refs/heads/{branch}", credential=credential
)
def discard_branch(self, branch: str, *, back_to: str) -> None:
"""Throw away a branch and everything on it.
@@ -159,6 +220,8 @@ class GitRepo:
self._git("branch", "-q", "-D", branch)
def delete_remote_branch(
self, remote_url: str, branch: str, *, key: str | None = None
self, remote_url: str, branch: str, *, credential: PushCredential | None = None
) -> None:
self._git("push", "-q", remote_url, f":refs/heads/{branch}", key=key)
self._git(
"push", "-q", remote_url, f":refs/heads/{branch}", credential=credential
)