Files
welcome-to-codevalet-as-a-c…/README.md
FanaticPythoner (Nathan Trudeau) a591cd21f2 Initial Commit
2026-04-19 17:11:58 -04:00

247 lines
11 KiB
Markdown

# welcome-to-codevalet-as-a-contributor
Onboarding for new contributors to the codevalet Gitea instance.
Produces an authenticated local checkout of
`forge-stack-orchestrator` with git operations against Gitea
authenticated silently through an OAuth2 (PKCE) token shared with the
orchestrator's devpi gateway.
## Quick start
```bash
just setup
```
`just setup` runs `doctor`, `init-env`, `check-gitea`, `login`,
`check-access`, and `clone-orchestrator` in sequence and prompts only
when a decision requires a human (missing Gitea username, existing
stored session, existing orchestrator clone). Idempotent.
Individual recipes remain available; see `just --list`.
## Prerequisites
Linux, macOS, or Windows-via-WSL with:
- `git`, `bash`, `curl`
- Python 3.11+ (a `uv python install 3.11` interpreter counts)
- [`just`](https://github.com/casey/just#installation)
- [`uv`](https://docs.astral.sh/uv/#installation)
- graphical web browser (optional; see *Headless and SSH hosts*)
- `~/.local/bin` on `PATH`
`just doctor` verifies all of these and, for each miss, prints the
exact install command for the detected platform (`apt` / `dnf` /
`pacman` / `zypper` / `brew`) and a consolidated block at the end. No
recipe runs `sudo` or installs anything system-wide.
A Gitea account on `FORGE_GITEA_URL` with membership in the
`codevalet` organisation is also required.
## Configuring `.env`
`just init-env` copies `.env.example``.env`. The only required
edit is `FORGE_GITEA_USERNAME`. Defaults:
| Variable | Default | Change when |
| --- | --- | --- |
| `FORGE_GITEA_URL` | `https://gitea.cvgitea.ddns.net:6006` | different Gitea instance |
| `FORGE_GITEA_ORG` | `codevalet` | fork or sibling org |
| `FORGE_ORCHESTRATOR_REPO_URL` | `.../codevalet/forge-stack-orchestrator.git` | different fork |
| `FORGE_ORCHESTRATOR_BRANCH` | *(empty: default branch)* | branch pin |
| `FORGE_WORKSPACE_ROOT` | `.` (clone at `./forge-stack-orchestrator`, gitignored) | clone elsewhere |
| `FSDGG_CLI_CLIENT_ID` | registered PKCE CLI client | never |
| `FSDGG_CLI_REDIRECT_URI` | `http://127.0.0.1:38111/callback` | port conflict only; must stay `http://` + loopback per RFC 8252 §7.3 (`127.0.0.1`, `[::1]`, or `localhost`) and match the Gitea OAuth app registration |
**`FSDGG_CLI_REDIRECT_URI` is not the Gitea URL.** The Gitea server is
at `FORGE_GITEA_URL` (remote, HTTPS). The redirect URI is the local
loopback HTTP listener the CLI binds on *your* machine so Gitea can
hand back the OAuth authorisation code; OAuth 2.0 for Native Apps
(RFC 8252 §7.3) prohibits any non-loopback / non-HTTP scheme here,
and no public CA will issue a cert for `127.0.0.1` so HTTPS on
loopback is not a meaningful option. On a shared or remote host,
SSH-forward the port (see *Headless and SSH hosts*) rather than
trying to publish the callback over the network.
`.env` is gitignored. OAuth client IDs are public by design; PKCE
requires no client secret.
## Recipes
| Recipe | Effect |
| --- | --- |
| `just setup` | Full interactive onboarding. `FORGE_SETUP_YES=1` accepts every default. |
| `just setup --headless` | Same, but skips `webbrowser.open`; alias `--no-browser`. |
| `just welcome` | Onboarding plan. |
| `just doctor` | Prerequisite checks with copy-pasteable fixes. |
| `just init-env` | Copies `.env.example``.env`. Never overwrites. |
| `just check-gitea` | Hits `FORGE_GITEA_URL/api/v1/version`. |
| `just login` | Browser PKCE OAuth2 flow; installs the git credential helper. |
| `just login-headless` | `just login` without opening a browser. |
| `just status` | Stored-token state. Exits non-zero when not live. |
| `just refresh` | Forces a token refresh (normally automatic). |
| `just logout` | Clears stored Gitea tokens; keeps the credential helper and orchestrator-gateway fields. |
| `just relogin` | `logout` + `login`. |
| `just check-access` | `git ls-remote` against the orchestrator. |
| `just clone-orchestrator` | Clones into `$FORGE_WORKSPACE_ROOT`. Idempotent. |
| `just next-steps` | Commands to run inside the orchestrator checkout. |
| `just uninstall` | Reverses `login` and `clone-orchestrator`. |
| `just test` | Full test suite. |
## Switching Gitea users
```bash
just relogin # or: just logout && just login
```
`just logout` leaves the credential helper installed and preserves
orchestrator-gateway fields in
`~/.forge-stack-devpi-gateway-gitea/client-auth.json`. Git operations
against `FORGE_GITEA_URL` fail loudly between `logout` and the next
`login`.
## Wrong-user guard
`just login` refuses to persist tokens that do not match
`FORGE_GITEA_USERNAME`:
1. The authorise URL carries `prompt=login` (OIDC Core §3.1.2.1),
forcing Gitea to re-display its login form even under an active
session; when `FORGE_GITEA_USERNAME` is set, `login_hint=<username>`
is also included.
2. After the token exchange, `userinfo.preferred_username` is compared
case-insensitively to `FORGE_GITEA_USERNAME`. On mismatch the token
is discarded, the auth file is left untouched, the Gitea logout
URL (`<FORGE_GITEA_URL>/user/logout?redirect_to=/user/login`) is
opened in the browser, and the CLI exits non-zero with recovery
steps.
## After `just clone-orchestrator`
```bash
cd ./forge-stack-orchestrator # or $FORGE_WORKSPACE_ROOT/<repo>
just bootstrap
just repos-sync
just dev-env-refresh
just test-all
```
`just login` already wrote the OAuth token to the shared auth file, so
the orchestrator's recipes reuse it without a second login. If the
orchestrator reports a missing gateway bearer, run `just repos-login`
inside the orchestrator checkout; it layers a gateway bearer on top of
the existing Gitea token without a second Gitea login.
## `just login` internals
1. Reads `FORGE_GITEA_URL`, `FSDGG_CLI_CLIENT_ID`, optional
`FORGE_GITEA_USERNAME` from `.env`.
2. GET `<gitea>/.well-known/openid-configuration`; no endpoints are
hardcoded.
3. PKCE verifier (`secrets.token_urlsafe(64)`) and S256 challenge
(RFC 7636).
4. HMAC-signed CSRF `state`; session key held in process memory only.
5. Authorise URL carries `prompt=login` and, if set,
`login_hint=<FORGE_GITEA_USERNAME>`.
6. Opens the default browser (or prints the URL under `--no-browser`)
and starts a one-shot loopback HTTP listener on the port from
`FSDGG_CLI_REDIRECT_URI`.
7. Verifies the returned state, POSTs the code + verifier to the token
endpoint, receives `access_token`, `refresh_token`, `expires_in`.
8. GET `/login/oauth/userinfo` for the authenticated username.
9. If `FORGE_GITEA_USERNAME` is set and does not match, discard the
token and open the Gitea logout URL (see *Wrong-user guard*).
10. Writes `~/.forge-stack-devpi-gateway-gitea/client-auth.json`
atomically (`.tmp` + `chmod 0600` + `rename`). Gateway-owned
fields are preserved.
11. Installs `git-credential-forge` + `forge_auth.py` under
`~/.local/bin/` and sets `git config --global
credential.<FORGE_GITEA_URL>.helper`. Scope is `FORGE_GITEA_URL`
only; other git hosts are untouched.
## Token refresh
Access-token lifetime is set by Gitea (typically one hour). When it
expires the credential helper calls the token endpoint with the
stored refresh token and retries the git operation once; no user
interaction. When the refresh token also expires (Gitea default: 30
days), the helper emits a one-line pointer at `just login` and the
git operation fails.
## Headless and SSH hosts
```bash
just setup --headless # full onboarding, no webbrowser.open
just login-headless # login step only
```
Both print the authorise URL on stderr. Paste it into any browser
that can reach the loopback callback port (`FSDGG_CLI_REDIRECT_URI`,
default `38111`). For a remote host:
```bash
ssh -L 38111:127.0.0.1:38111 <host>
# then, on <host>:
just setup --headless
```
`--no-browser` is an alias for `--headless`.
`just setup --headless` refreshes silently when the access token is
stale but the refresh token is still valid, avoiding the URL-paste
step entirely.
Combining `--headless` with `FORGE_SETUP_YES=1` while a fresh browser
OAuth flow is required is contradictory: the authorise URL must be
pasted into a browser, but `FORGE_SETUP_YES=1` forbids interaction.
`just setup` detects this and exits immediately rather than hanging.
Either drop `FORGE_SETUP_YES=1`, or run `just login` once on a host
with a browser to populate a valid refresh token before running
`just setup --headless` on the headless host.
## Troubleshooting
| Symptom | Resolution |
| --- | --- |
| `just doctor` reports a missing tool | Run the `fix:` command printed beside it. |
| `~/.local/bin` not on `PATH` | Add `export PATH="$HOME/.local/bin:$PATH"` to the shell rc and reopen. |
| `just check-gitea` → connection refused | Verify `FORGE_GITEA_URL` and network access. |
| `just login` → browser does not open | Run `just login-headless`. |
| `just login` → timed out waiting for OAuth callback | Consent was not completed in the browser; re-run. |
| `just login` → cannot bind `127.0.0.1:38111` | Another `just login` is running; wait or kill it. |
| "Why is the redirect URI `http://127.0.0.1`? The gateway is remote and HTTPS." | `FSDGG_CLI_REDIRECT_URI` is the CLI's local loopback listener, not the Gitea or gateway URL. OAuth 2.0 for Native Apps (RFC 8252 §7.3) requires `http` on a loopback address. The Gitea server (`FORGE_GITEA_URL`) is the remote HTTPS endpoint, reached during the authorise step. On a remote host, SSH-forward the redirect port. |
| `just setup --headless` → "cannot complete a fresh login under --headless + FORGE_SETUP_YES=1" | Drop `FORGE_SETUP_YES=1`, or run `just login` once on a host with a browser to populate a refresh token, then re-run `just setup --headless`. |
| `just login` rejects with a username-mismatch error | Follow the logout link printed, sign in as `FORGE_GITEA_USERNAME`, re-run. |
| `just check-access``Repository not found` | Account not in the `codevalet` org yet. |
| `just check-access` → asks for a password | `just login` did not complete. Re-run. |
| Git prompts for a password on pull/push | Refresh token expired. Run `just relogin`. |
| `just status` shows `live: False` | Run `just refresh`; also happens automatically on the next git op. |
| `just clone-orchestrator` prints `already cloned` | Intended; idempotent. |
| Want a clean slate | `just uninstall`. |
## Security properties
- Only a public OAuth client ID ships in `.env.example`; PKCE removes
the need for a client secret.
- `.env` holds configuration only. Tokens live in
`~/.forge-stack-devpi-gateway-gitea/client-auth.json` at mode `0600`.
- The credential helper is scoped to `FORGE_GITEA_URL`; requests for
other hosts flow through git's normal credential chain.
- The OAuth `state` is HMAC-signed with an in-memory session key; a
replayed state from another session does not verify.
- Writes to the auth file are atomic (`.tmp` + `rename`); a crash
during `just login` or `just refresh` leaves the previous valid
state intact.
- `just check-access` and `just clone-orchestrator` neuter
`GIT_TERMINAL_PROMPT`, `GIT_ASKPASS`, and the VSCode / X11 askpass
variables so auth failures surface loudly instead of triggering GUI
credential prompts.
## Tests
```bash
just test
```
See `tests/README.md`.