From dd58cd9dad7ae7395d65e81b32c7f5628a2e86be Mon Sep 17 00:00:00 2001 From: alexion Date: Sun, 19 Jul 2026 09:41:13 -0400 Subject: [PATCH] fix(context): tolerate a trailing /api/v1 in the base URL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The client (gitea-js) appends /api/v1 to the base URL itself, so a GITEA_AXI_API_URL that already carries it — a natural guess given the variable's name — doubled the segment and failed as a spurious REPO_NOT_FOUND. Normalize the base URL by stripping a trailing /api/v1 (and any trailing slashes) on both the env-URL and tea-login paths, so the host base and the /api/v1 endpoint both resolve. --- src/context.ts | 18 ++++++++++++++--- test/context.test.ts | 48 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/src/context.ts b/src/context.ts index 1049b78..120d487 100644 --- a/src/context.ts +++ b/src/context.ts @@ -60,6 +60,17 @@ function hostnameOf(url: string, origin: string): string { } } +/** + * Normalize a Gitea base URL to the host root the client expects. The client + * (gitea-js) appends `/api/v1` itself, so a value that already carries it — a + * natural guess when the variable is literally named `..._API_URL` — would double + * the segment and 404 as a spurious `REPO_NOT_FOUND`. Strip a trailing `/api/v1` + * (with any trailing slashes) so the host base and the API endpoint both work. + */ +function normalizeApiBase(url: string): string { + return url.replace(/\/+$/, "").replace(/\/api\/v1$/, ""); +} + function resolveTestModeContext( deps: CliDeps, apiUrl: string, @@ -72,10 +83,11 @@ function resolveTestModeContext( ["Set `GITEA_AXI_REPO=OWNER/NAME` or pass `-R OWNER/NAME`"], ); } + const base = normalizeApiBase(apiUrl); return { ...parseRepoSpec(overrides.repoSpec, overrides.repoOrigin), - host: hostnameOf(apiUrl, "`GITEA_AXI_API_URL`"), - apiUrl: apiUrl.replace(/\/+$/, ""), + host: hostnameOf(base, "`GITEA_AXI_API_URL`"), + apiUrl: base, token: deps.env.GITEA_AXI_TOKEN ?? "", repoSource: overrides.repoSource, loginSource: overrides.loginSource, @@ -184,7 +196,7 @@ export async function resolveRepoContext(deps: CliDeps): Promise { owner, name, host, - apiUrl: login.url.replace(/\/+$/, ""), + apiUrl: normalizeApiBase(login.url), token, repoSource: overrides.repoSource, loginSource: overrides.loginSource, diff --git a/test/context.test.ts b/test/context.test.ts index 1e2ec3f..55282c0 100644 --- a/test/context.test.ts +++ b/test/context.test.ts @@ -1,4 +1,6 @@ import { afterEach, describe, expect, it } from "vitest"; +import { resolveRepoContext } from "../src/context.js"; +import type { CliDeps } from "../src/deps.js"; import { startFixtureServer, type FixtureServer } from "./fixture-server.js"; import { runCliTest, testModeEnv } from "./harness.js"; @@ -117,3 +119,49 @@ describe("context overrides", () => { expect(exitCode).toBe(0); }); }); + +describe("apiUrl normalization", () => { + function depsWithApiUrl(apiUrl: string): CliDeps { + return { + env: { + GITEA_AXI_API_URL: apiUrl, + GITEA_AXI_REPO: "acme/widgets", + GITEA_AXI_TOKEN: "test-token", + }, + cwd: process.cwd(), + globals: {}, + }; + } + + it("strips a trailing /api/v1 suffix from the host base", async () => { + const context = await resolveRepoContext( + depsWithApiUrl("https://git.example.com/api/v1"), + ); + + expect(context.apiUrl).toBe("https://git.example.com"); + }); + + it("strips a trailing /api/v1/ with a trailing slash", async () => { + const context = await resolveRepoContext( + depsWithApiUrl("https://git.example.com/api/v1/"), + ); + + expect(context.apiUrl).toBe("https://git.example.com"); + }); + + it("leaves a host base without an /api/v1 suffix unchanged", async () => { + const context = await resolveRepoContext( + depsWithApiUrl("https://git.example.com"), + ); + + expect(context.apiUrl).toBe("https://git.example.com"); + }); + + it("strips a lone trailing slash from the host base", async () => { + const context = await resolveRepoContext( + depsWithApiUrl("https://git.example.com/"), + ); + + expect(context.apiUrl).toBe("https://git.example.com"); + }); +});