kc-ship-pr · diff
git:20260728.a0535b4 to git:20260908.351228a
12 added, 2 removed. Audit A to A.
---
name: kc-ship-pr
description: Commit local changes and open a pull request against kube-coder from inside a workspace pod. Use when the user wants to push a branch or open/update a PR with the workspace GitHub App token.
user-invocable: true
allowed-tools: Bash, Read
argument-hint: "[PR title] (optional; inferred from the commit if omitted)"
---
# Ship a PR from a kube-coder workspace
The only GitHub credential here is the App installation token
(`/home/dev/.credentials/.github-token`, `ghs_…`). The token-refresh sidecar
installs a **self-refreshing global `credential.helper`** that reads that file
fresh on every call, so ordinary **`git push` works** — no Git Data API dance
needed. `gh api` also works (it reads `GITHUB_TOKEN` from `~/.github-env`).
There is no user-level `gh auth login` and no SSH key here, so do **not** suggest
`gh auth login` or a fork — the App token pushes to `origin` directly.
> **If `git push` is wedged** — `remote: Invalid username or token` or
> `could not read Username` — something is shadowing that helper with a
> point-in-time token. The token file itself is almost never the problem
> (`curl -H "Authorization: token $(cat /home/dev/.credentials/.github-token)"
> https://api.github.com/user` proves it in one line). Diagnose in this order:
>
> ```bash
> git config --show-origin --get-all credential.helper
> git config --show-origin --get-all credential.https://github.com.helper
> printf 'protocol=https\nhost=github.com\n\n' | git credential fill # vs. the token file
> grep github.com ~/.git-credentials
> ```
>
> Three known causes, all "a token frozen an hour ago":
> 1. A **stale baked `http.<host>.extraheader`** in `.git/config` — git sends it
> verbatim and it shadows the helper: `git config --unset-all
> http.https://github.com/.extraheader`.
> 2. A **host-scoped helper chain reset** (issue #454) — `gh auth setup-git`
> writes an empty `helper =` under `[credential "https://github.com"]`, which
> *clears* the chain and drops the self-refreshing reader; `gh auth
> git-credential` then answers with the **stale `GH_TOKEN`** your long-lived
> shell captured before the last rotation. The workspace re-asserts that
> section every 50 min, so this self-heals — to fix it now, run
> `python3 /github-app/github-app-token.py --configure-git` (or `--once`,
> which also asks the `github-app-token` sidecar for a fresh token; the
> private key lives only in that sidecar since #558, so nothing in your
> shell can mint one).
> 3. A **stale `github.com` line in `~/.git-credentials`** (persistent on the
> PVC, so a captured `ghs_…` outlives its 1-hour validity). The daemon purges
> these in app mode; delete by hand if you're ahead of it.
>
> One-shot escape hatch that bypasses all three:
> ```bash
> git -c credential.helper= \
> -c 'credential.helper=!f() { echo username=x-access-token; echo "password=$(cat /home/dev/.credentials/.github-token)"; }; f' \
> push -u origin <branch>
> ```
> Only if push is *still* broken after that, fall back to the Git Data API
> (§3b). See your `github-auth` memory for the full background.
## Steps
Given the working tree already has the changes staged/committed on a feature
branch (create one first if the user is on `main`):
### 1. Commit locally
```bash
cd /home/dev/kube-coder # or your worktree
git add -A # or specific paths
git commit -m "<type>(<scope>): <summary>
<body>
Co-Authored-By: Claude <noreply@anthropic.com>"
```
### 2. Re-sync onto current origin/main (avoid a stale base)
```bash
git fetch origin main && git rebase origin/main
```
If the credential helper is somehow unavailable, the repo is public so an
unauthenticated fetch also works:
`git -c credential.helper= -c http.https://github.com/.extraheader= fetch https://github.com/imran31415/kube-coder.git main`
### 3. Push the branch
```bash
git push -u origin <branch>
```
### 3b. Fallback — push via the Git Data API (only if `git push` stays wedged)
Run this Python (token from env or the credential file). Set `BRANCH` and list
the changed files in `FILES`:
```bash
source /home/dev/.credentials/.github-env # exports GITHUB_TOKEN
python3 - <<'PY'
import os, json, base64, urllib.request, subprocess
TOKEN = os.environ["GITHUB_TOKEN"]; REPO = "imran31415/kube-coder"
API = f"https://api.github.com/repos/{REPO}"
BRANCH = "REPLACE-branch-name"
FILES = subprocess.check_output(
["git", "diff", "--name-only", "main", "HEAD"]).decode().split()
def api(method, url, body=None):
d = json.dumps(body).encode() if body is not None else None
r = urllib.request.Request(url, data=d, method=method)
r.add_header("Authorization", f"token {TOKEN}")
r.add_header("Accept", "application/vnd.github+json")
if d: r.add_header("Content-Type", "application/json")
with urllib.request.urlopen(r) as x: return json.load(x)
base = api("GET", f"{API}/git/refs/heads/main")["object"]["sha"]
tree = api("GET", f"{API}/git/commits/{base}")["tree"]["sha"]
entries = []
for f in FILES:
blob = api("POST", f"{API}/git/blobs",
{"content": base64.b64encode(open(f, "rb").read()).decode(),
"encoding": "base64"})
entries.append({"path": f, "mode": "100644", "type": "blob", "sha": blob["sha"]})
t = api("POST", f"{API}/git/trees", {"base_tree": tree, "tree": entries})
msg = subprocess.check_output(["git", "log", "-1", "--format=%B"]).decode().strip()
commit = api("POST", f"{API}/git/commits",
{"message": msg, "tree": t["sha"], "parents": [base]})
# New branch: POST a ref. To UPDATE an existing branch (add a commit), use
# PATCH {API}/git/refs/heads/{BRANCH} with {"sha": commit["sha"]} instead.
api("POST", f"{API}/git/refs",
{"ref": f"refs/heads/{BRANCH}", "sha": commit["sha"]})
print("pushed", commit["sha"][:8], "->", BRANCH)
PY
```
Notes:
- **New branch** → `POST /git/refs`. **Add a commit to an existing PR branch**
→ read the branch's current head first, base the tree on it, then
`PATCH /git/refs/heads/{BRANCH}` with the new commit sha.
- The commit author is the App (bot), not the user — expected and fine for a PR.
- Only the files in `FILES` change; everything else is inherited from `base_tree`.
### 4. Open the PR
```bash
source /home/dev/.credentials/.github-env
BODY=$(cat <<'EOF'
## What & why
...
## Testing
...
🤖 Generated with [Claude Code](https://claude.com/claude-code)
EOF
)
gh api repos/imran31415/kube-coder/pulls \
-f title="$ARGUMENTS" -f head="REPLACE-branch-name" -f base="main" -f body="$BODY" \
--jq '.html_url'
```
If `gh` fails, the same works with `curl -X POST -H "Authorization: token $GITHUB_TOKEN"`.
### 5. Link the issue (if the PR resolves one)
The PR body/title referencing `(#N)` cross-links but does **not** auto-close.
To close on merge, add `Fixes #N` to the PR body, or close the issue after merge:
```bash
gh api repos/imran31415/kube-coder/issues/N/comments -f body="Resolved by #<pr>."
gh api -X PATCH repos/imran31415/kube-coder/issues/N -f state=closed -f state_reason=completed
```
## Before shipping
- Run **kc-preflight** first so CI is green on the first push. Never push a branch
- you haven't at least `bash -n`/typecheck/test-run locally — CI round-trips are slow.
+ Two skills, in this order:
+ 1. **kc-scope-pr** — what the diff actually reaches, which tests cover it, what
+ it made worse, what it left untested. Its `impacted=` and `untested=` numbers
+ belong in the PR body when they are large: a reviewer who sees "3 files
+ changed" reads the diff differently than one who also sees the blast radius.
+ 2. **kc-preflight** — runs the suites for real, so CI is green on the first push.
+
+ Never push a branch you haven't at least `bash -n`/typecheck/test-run locally —
+ CI round-trips are slow.
+
## See also
+ - **kc-scope-pr** for the numbers that make a PR body reviewable, and the tests
+ to run before this one.
- Your `github-auth` memory has the full background on the App-token auth setup
and the stale-extraheader footgun.
- To add a commit to an already-open PR, just `git push` again; if you're using
the §3b fallback, repeat it in PATCH mode onto the branch head.