247 lines
11 KiB
Markdown
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`.
|