docs(releasing): full gh auth login section

- gh's credential is separate from git's: SSH keys let you push but
  not call the API, so a machine that pushes can still fail to release
- the five interactive prompts and what to answer, with SSH as the
  protocol to match this repo's existing remote
- headless path: PAT scopes (classic repo/read:org/gist, fine-grained
  Contents: read and write), --with-token from a 600 file, GH_TOKEN
  for automation
- verify with `gh repo view shoneyJ/writeonce` — proves the token
  reaches THIS repo, not just that it is valid

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
shoney.arickathil 2026-08-25 04:54:57 +02:00
parent b11f964eec
commit 844bcc1067

View file

@ -16,17 +16,69 @@ tag must be `v0.1.0` and the asset must be named exactly
`writeonce-0.1.0-linux-amd64.tar.gz` — which is what `just dist` already `writeonce-0.1.0-linux-amd64.tar.gz` — which is what `just dist` already
produces. produces.
## 0. Before you start ## 0. Authenticate `gh` (once per machine)
`gh` must be authenticated. It is an interactive browser/device flow, so `gh` keeps its own credential, separate from git's. SSH keys let you
run it yourself: `git push`; they do **not** let `gh` call the API, so a machine that
pushes fine can still fail to create a release.
Check first — if this names your account, skip the rest of this section:
```
gh auth status
```
### The interactive flow (what to pick)
``` ```
gh auth login gh auth login
``` ```
In a Claude Code session, type it with a leading `!` so the output lands It asks five things:
in the conversation: `! gh auth login`.
| prompt | answer |
| --- | --- |
| What account do you want to log into? | **GitHub.com** |
| Preferred protocol for Git operations? | **SSH** — this repo's remote is already `git@github.com:shoneyJ/writeonce.git`, and answering HTTPS here rewrites how git talks to GitHub for every repo on the host |
| Upload your SSH public key? | pick `~/.ssh/id_ed25519.pub`, or **Skip** if that key is already on your account |
| How would you like to authenticate? | **Login with a web browser** |
| One-time code | gh prints something like `ABCD-1234`; press Enter, paste it at <https://github.com/login/device>, authorise |
The token lands in the system credential store, or in a plain file if
there is no store (`gh auth status` prints the location; `--insecure-storage`
forces the file). `-w`/`--web` skips straight to the browser step.
**In a Claude Code session, prefix it with `!`** — `! gh auth login` — so
the prompts are yours to answer and the output lands in the conversation.
An agent cannot complete a device flow on your behalf.
### No browser on that machine (server, container, CI)
Create a personal access token at <https://github.com/settings/tokens>.
Classic tokens need the scopes `repo`, `read:org` and `gist`;
a fine-grained token needs **Contents: read and write** on the repo,
which is what release assets are written through. Then:
```
printf '%s' "$TOKEN" | gh auth login --with-token
```
Read it from a `chmod 600` file rather than typing it inline — an
argument on the command line lands in shell history and in `ps`. For
automation, skip login entirely and export `GH_TOKEN`; gh picks it up and
stores nothing.
### Verify before you rely on it
```
gh auth status
gh repo view shoneyJ/writeonce --json name,visibility
```
The second call is the real check: it proves the token can reach *this*
repo, which a valid-but-wrong-account token would not.
## 1. Before you build
Check the tree is clean and the version is what you mean to ship — Check the tree is clean and the version is what you mean to ship —
`VERSION` is the single source, and `mkdist.sh` refuses to build if `VERSION` is the single source, and `mkdist.sh` refuses to build if
@ -37,7 +89,7 @@ git status --porcelain # expect empty
cat VERSION # e.g. 0.1.0 cat VERSION # e.g. 0.1.0
``` ```
## 1. Build the artifact ## 2. Build the artifact
``` ```
just dist just dist
@ -51,7 +103,7 @@ dist/writeonce-<ver>-linux-amd64.tar.gz
dist/writeonce-<ver>-linux-amd64.tar.gz.sha256 dist/writeonce-<ver>-linux-amd64.tar.gz.sha256
``` ```
## 2. Verify it before anyone else can ## 3. Verify it before anyone else can
Two checks, both cheap, both worth it. First the digest: Two checks, both cheap, both worth it. First the digest:
@ -76,7 +128,7 @@ woc . && ./target/hello
If that prints `hello, writeonce`, the release is sound. If that prints `hello, writeonce`, the release is sound.
## 3. Tag the commit ## 4. Tag the commit
The tag is what the download URL points at, so tag the exact commit the The tag is what the download URL points at, so tag the exact commit the
binaries were built from: binaries were built from:
@ -86,7 +138,7 @@ git tag -a v0.1.0 -m "writeonce 0.1.0"
git push origin v0.1.0 git push origin v0.1.0
``` ```
## 4. Publish and upload ## 5. Publish and upload
One command creates the release and attaches both files: One command creates the release and attaches both files:
@ -120,7 +172,7 @@ page → *Draft a new release* → choose tag `v0.1.0` → drag both files
into the attachment box → *Publish release*. Uploading by hand is where into the attachment box → *Publish release*. Uploading by hand is where
the filename usually drifts, so paste it rather than retyping it. the filename usually drifts, so paste it rather than retyping it.
## 5. Check the link the site actually uses ## 6. Check the link the site actually uses
``` ```
curl -sIL -o /dev/null -w '%{http_code} %{url_effective}\n' \ curl -sIL -o /dev/null -w '%{http_code} %{url_effective}\n' \
@ -131,7 +183,7 @@ A `200` means `/install`'s download button works for a stranger. Anything
else means the tag, the asset name, or the repo path disagrees with the else means the tag, the asset name, or the repo path disagrees with the
link in `docs/examples/site/install/view.wo`. link in `docs/examples/site/install/view.wo`.
## 6. Refresh the site's mirror ## 7. Refresh the site's mirror
`/install` offers this site as a mirror beside the GitHub link, served by `/install` offers this site as a mirror beside the GitHub link, served by
the framework's `StaticFiles` out of `$WO_DIST` (default `./dist`). Copy the framework's `StaticFiles` out of `$WO_DIST` (default `./dist`). Copy