feat(forge): adapter seam + Gitea implementation — optional read access to the operator's forge (#2689)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / integration (push) Successful in 19s
CI & Build / TypeScript typecheck (push) Successful in 34s
CI & Build / Python tests (push) Successful in 55s
CI & Build / Build & push image (push) Successful in 36s
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / integration (push) Successful in 19s
CI & Build / TypeScript typecheck (push) Successful in 34s
CI & Build / Python tests (push) Successful in 55s
CI & Build / Build & push image (push) Successful in 36s
Step 4 of milestone 288 (decision #2686). services/forge.py defines the contract steps 5-7 consume — read_file (content + last_commit_sha, the provenance stamp), default_branch, resolve_repo, check — with GiteaForge as the first implementation over the REST contents/repo/version/user endpoints. Repo identity reuses normalize_repo_key: the host segment selects whether this forge serves a recorded repo, the remainder is the API path, so no new identity scheme exists. Read-only by construction; errors never carry the token; first outbound-HTTP timeout convention (5s total, no retries — the consumer's fallback is the retry policy). OPTIONAL per instance (rule #115): get_forge() returns None when unconfigured and every consumer treats None as today's behavior. Config lives in admin settings (Settings → Config → Git Forge: kind/base URL/token, save + test-connection probe reporting version + identity), with FORGE_* env / Docker-secret fallbacks; DB wins so a UI edit can't silently lose to an env var. Token treatment follows the smtp_password convention (masked on read, mask-sentinel skipped on write, absent from audit details) — and wiring it surfaced that the generic GET/PUT /api/settings dump bypassed that masking for the owning admin's raw KV rows, so secret keys are now masked there too (fixes the same exposure for smtp_password). Contract tests run against httpx.MockTransport as the fake forge — the reference behaviors the GitHub adapter (step 8) must reproduce — plus the off-by-default gate, partial-config-is-off, env-vs-DB precedence, and route/mask structural checks. Also: the step-2 definition detector learned to skip dunders after flagging __init__ as 'already defined in 4 files' on this step's own build — guaranteed noise for a hint that must stay trustworthy. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -420,6 +420,16 @@ const baseUrl = ref("");
|
||||
const savingBaseUrl = ref(false);
|
||||
const baseUrlSaved = ref(false);
|
||||
|
||||
// Git forge integration (admin only, #2689). The token round-trips masked;
|
||||
// the server treats the mask as "unchanged".
|
||||
const forge = ref({ kind: "", base_url: "", token: "" });
|
||||
const forgeKinds = ref<string[]>(["gitea"]);
|
||||
const forgeConfigured = ref(false);
|
||||
const savingForge = ref(false);
|
||||
const forgeSaved = ref(false);
|
||||
const testingForge = ref(false);
|
||||
const forgeTestResult = ref<{ ok: boolean; message: string } | null>(null);
|
||||
|
||||
|
||||
// Search test (SearXNG)
|
||||
const searxngConfigured = ref(false);
|
||||
@@ -565,10 +575,25 @@ onMounted(async () => {
|
||||
} catch {
|
||||
// base URL not configured yet
|
||||
}
|
||||
try {
|
||||
await loadForgeSettings();
|
||||
} catch {
|
||||
// forge not configured yet
|
||||
}
|
||||
}
|
||||
_loadTabContent(activeTab.value);
|
||||
});
|
||||
|
||||
async function loadForgeSettings() {
|
||||
const cfg = await apiGet<{
|
||||
kind: string; base_url: string; token: string;
|
||||
configured: boolean; kinds: string[];
|
||||
}>("/api/admin/forge");
|
||||
forge.value = { kind: cfg.kind, base_url: cfg.base_url, token: cfg.token };
|
||||
forgeConfigured.value = cfg.configured;
|
||||
if (cfg.kinds?.length) forgeKinds.value = cfg.kinds;
|
||||
}
|
||||
|
||||
async function changeEmail() {
|
||||
changingEmail.value = true;
|
||||
try {
|
||||
@@ -734,6 +759,45 @@ async function sendTestEmail() {
|
||||
}
|
||||
}
|
||||
|
||||
async function saveForge() {
|
||||
savingForge.value = true;
|
||||
forgeSaved.value = false;
|
||||
forgeTestResult.value = null;
|
||||
try {
|
||||
await apiPut("/api/admin/forge", forge.value);
|
||||
await loadForgeSettings();
|
||||
forgeSaved.value = true;
|
||||
setTimeout(() => (forgeSaved.value = false), 2000);
|
||||
} catch (e) {
|
||||
const body = (e as { body?: { error?: string } }).body;
|
||||
toastStore.show(body?.error || "Failed to save forge settings", "error");
|
||||
} finally {
|
||||
savingForge.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function testForge() {
|
||||
testingForge.value = true;
|
||||
forgeTestResult.value = null;
|
||||
try {
|
||||
const res = await apiPost<{ version: string; username: string }>(
|
||||
"/api/admin/forge/test", {},
|
||||
);
|
||||
forgeTestResult.value = {
|
||||
ok: true,
|
||||
message: `Connected — Gitea ${res.version}, authenticated as ${res.username}`,
|
||||
};
|
||||
} catch (e) {
|
||||
const body = (e as { body?: { error?: string } }).body;
|
||||
forgeTestResult.value = {
|
||||
ok: false,
|
||||
message: body?.error || "Connection test failed",
|
||||
};
|
||||
} finally {
|
||||
testingForge.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveBaseUrl() {
|
||||
savingBaseUrl.value = true;
|
||||
baseUrlSaved.value = false;
|
||||
@@ -2090,6 +2154,47 @@ function formatUserDate(iso: string): string {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settings-section full-width">
|
||||
<h2>Git Forge</h2>
|
||||
<p class="section-desc">
|
||||
Optional read-only connection to your git forge (Gitea) so snippet
|
||||
code can be fetched and drift-checked server-side. A read-scope API
|
||||
token is enough. Leave the kind unset to keep the integration off.
|
||||
</p>
|
||||
<div class="smtp-grid">
|
||||
<div class="field">
|
||||
<label for="forge-kind">Forge</label>
|
||||
<select id="forge-kind" v-model="forge.kind" class="input">
|
||||
<option value="">Off</option>
|
||||
<option v-for="k in forgeKinds" :key="k" :value="k">{{ k }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="forge-base-url">Base URL</label>
|
||||
<input id="forge-base-url" v-model="forge.base_url" type="text" placeholder="https://git.example.com" class="input" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="forge-token">API Token (read scope)</label>
|
||||
<input id="forge-token" v-model="forge.token" type="password" class="input" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions" style="margin-bottom: 1.25rem;">
|
||||
<button class="btn-primary" @click="saveForge" :disabled="savingForge">
|
||||
{{ savingForge ? "Saving..." : "Save Forge Settings" }}
|
||||
</button>
|
||||
<button class="btn-ghost" @click="testForge" :disabled="testingForge || !forgeConfigured">
|
||||
{{ testingForge ? "Testing..." : "Test Connection" }}
|
||||
</button>
|
||||
<span v-if="forgeSaved" class="saved-msg">Saved!</span>
|
||||
</div>
|
||||
<p
|
||||
v-if="forgeTestResult"
|
||||
:class="forgeTestResult.ok ? 'text-success' : 'text-error'"
|
||||
>
|
||||
{{ forgeTestResult.message }}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ── Users ── -->
|
||||
|
||||
Reference in New Issue
Block a user