---
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

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.
