12 KiB
welcome-to-codevalet-as-a-contributor
Onboarding for new contributors to the codevalet Gitea instance.
The production Gitea host is published through the Pangolin HTTPS edge at
https://gitea.cvgitea.ddns.net.
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
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.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 runs sudo or installs anything system-wide.
A Gitea account on FORGE_GITEA_URL with membership in the
codevalet organisation is also required.
Current production value: https://gitea.cvgitea.ddns.net.
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 |
different Gitea instance |
FORGE_GITEA_ORG |
codevalet |
fork or sibling org |
FORGE_ORCHESTRATOR_REPO_URL |
https://gitea.cvgitea.ddns.net/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.
FORGE_GITEA_URL is the canonical public HTTPS endpoint. Do not append
:6006; Pangolin terminates TLS on the public edge and the onboarding
repo, orchestrator clone URL, and global git credential scope all assume
the standard portless form.
.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
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 clone-orchestrator
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
- 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 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:
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, confirm it is https://gitea.cvgitea.ddns.net for production, and do not append :6006. |
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. |
just login 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 just login. Required only once after a scope-set change. Full reference: docs/oauth-grant-scope-mismatch.md. |
| Reset local state | 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.
Tests
just test
See tests/README.md.