Nathan Trudeau a866a498e7 Add test for TTY
2026-05-04 13:38:35 -04:00
2026-05-03 07:25:31 -04:00
2026-05-04 13:38:35 -04:00
2026-04-29 08:15:20 -04:00
2026-04-27 15:56:43 -04:00
2026-04-30 17:38:09 -04:00
2026-04-29 08:15:20 -04:00

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.11 interpreter counts)
  • just
  • uv
  • 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 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:

  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:

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

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-accessRepository 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

just test

See tests/README.md.

Description
No description provided
Readme 125 KiB
Languages
Python 59.2%
Shell 37%
Just 3.8%