Initial Commit
This commit is contained in:
246
README.md
246
README.md
@@ -1,2 +1,246 @@
|
||||
# welcome-to-codevalet--developer
|
||||
# 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`.
|
||||
|
||||
Reference in New Issue
Block a user