Initial Commit
This commit is contained in:
266
README.md
266
README.md
@@ -1,2 +1,268 @@
|
||||
# welcome-to-codevalet-as-a-platform-operator
|
||||
|
||||
Onboarding for platform operators of 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, then hands off to the orchestrator's
|
||||
operator-setup runner.
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
just deploy # interactive
|
||||
just deploy --yes # auto-accept every prompt (session reuse, checkout reuse, handoff)
|
||||
```
|
||||
|
||||
`just deploy` runs `doctor`, `init-env`, `check-gitea`, `login`,
|
||||
`check-access`, `clone-orchestrator`, and then offers to execute the
|
||||
orchestrator's `operator-setup` runner. It prompts only when a
|
||||
decision requires a human (missing Gitea username, existing stored
|
||||
session, existing orchestrator clone, final go-ahead for
|
||||
`operator-setup`). Idempotent.
|
||||
|
||||
`just setup` is the same flow minus the `operator-setup` handoff:
|
||||
it stops at `clone-orchestrator`, which is the contributor entry
|
||||
point. Operators almost always want `just deploy`.
|
||||
|
||||
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 in this scaffold runs `sudo` or installs anything system-wide;
|
||||
any additional host setup required by the orchestrator's runner is
|
||||
handled inside the orchestrator checkout itself.
|
||||
|
||||
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 the local 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 deploy` | Full interactive onboarding + orchestrator `operator-setup` handoff. `FORGE_SETUP_YES=1` accepts every default. |
|
||||
| `just deploy --headless` | Same, but skips `webbrowser.open`; alias `--no-browser`. |
|
||||
| `just setup` | Onboarding only (stops at `clone-orchestrator`). |
|
||||
| `just setup --deploy` | Equivalent to `just deploy`; flag-style spelling. |
|
||||
| `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` | Prints the orchestrator's operator-setup plan. |
|
||||
| `just run-next-steps` | Executes that plan (== `just operator-setup` 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 deploy`
|
||||
|
||||
`just deploy` hands off to `just operator-setup` inside the
|
||||
orchestrator checkout; the authoritative runbook for every operator
|
||||
task lives there. `just setup` (without `--deploy`) stops after the
|
||||
clone and leaves the handoff for manual execution:
|
||||
|
||||
```bash
|
||||
cd ./forge-stack-orchestrator # or $FORGE_WORKSPACE_ROOT/<repo>
|
||||
just operator-setup
|
||||
```
|
||||
|
||||
`just login` already wrote the OAuth token to the shared auth file,
|
||||
and the orchestrator's recipes reuse it without a second login. When 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 deploy --headless # full onboarding + handoff, no webbrowser.open
|
||||
just setup --headless # onboarding only, no webbrowser.open
|
||||
just login-headless # login step only
|
||||
```
|
||||
|
||||
Each prints 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 deploy --headless
|
||||
```
|
||||
|
||||
`--no-browser` is an alias for `--headless`.
|
||||
|
||||
`just deploy --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 deploy` (and `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 deploy --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 deploy --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 deploy --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. |
|
||||
| `just deploy` runs fine through step 6 but the handoff fails | Open the orchestrator checkout and re-read its onboarding docs. This scaffold ends at `clone-orchestrator`; everything past it lives in the orchestrator. |
|
||||
| `just login` or `just deploy` exits with `Gitea server_error: "a grant exists with different scope"` | Run `just revoke-grant` (opens `<FORGE_GITEA_URL>/user/settings/applications` and prints the matching `FSDGG_CLI_CLIENT_ID`). Revoke the matching app, then re-run the failed recipe. Required only once after a scope-set change. Full reference: `docs/oauth-grant-scope-mismatch.md`. |
|
||||
| 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.
|
||||
- This scaffold never executes any privileged action on the host;
|
||||
its sole output is an authenticated orchestrator checkout, after
|
||||
which every operational task is the orchestrator's responsibility.
|
||||
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
just test
|
||||
```
|
||||
|
||||
See `tests/README.md`.
|
||||
|
||||
Reference in New Issue
Block a user