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
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.11interpreter counts) justuv- graphical web browser (optional; see Headless and SSH hosts)
~/.local/binonPATH
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
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:
- 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; whenFORGE_GITEA_USERNAMEis set,login_hint=<username>is also included. - After the token exchange,
userinfo.preferred_usernameis compared case-insensitively toFORGE_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:
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
- Reads
FORGE_GITEA_URL,FSDGG_CLI_CLIENT_ID, optionalFORGE_GITEA_USERNAMEfrom.env. - GET
<gitea>/.well-known/openid-configuration; no endpoints are hardcoded. - PKCE verifier (
secrets.token_urlsafe(64)) and S256 challenge (RFC 7636). - HMAC-signed CSRF
state; session key held in process memory only. - Authorise URL carries
prompt=loginand, if set,login_hint=<FORGE_GITEA_USERNAME>. - Opens the default browser (or prints the URL under
--no-browser) and starts a one-shot loopback HTTP listener on the port fromFSDGG_CLI_REDIRECT_URI. - Verifies the returned state, POSTs the code + verifier to the token
endpoint, receives
access_token,refresh_token,expires_in. - GET
/login/oauth/userinfofor the authenticated username. - If
FORGE_GITEA_USERNAMEis set and does not match, discard the token and open the Gitea logout URL (see Wrong-user guard). - Writes
~/.forge-stack-devpi-gateway-gitea/client-auth.jsonatomically (.tmp+chmod 0600+rename). Gateway-owned fields are preserved. - Installs
git-credential-forge+forge_auth.pyunder~/.local/bin/and setsgit config --global credential.<FORGE_GITEA_URL>.helper. Scope isFORGE_GITEA_URLonly; 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
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:
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. .envholds configuration only. Tokens live in~/.forge-stack-devpi-gateway-gitea/client-auth.jsonat mode0600.- The credential helper is scoped to
FORGE_GITEA_URL; requests for other hosts flow through git's normal credential chain. - The OAuth
stateis 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 duringjust loginorjust refreshleaves the previous valid state intact. just check-accessandjust clone-orchestratorneuterGIT_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
just test
See tests/README.md.