Files
fusion/packages/core/src/gitlab-config.ts
gsxdsm 573876603f FN-7422: add GitLab URL configuration
Add typed GitLab instance and API URL settings for GitLab.com and self-managed instances.

- Add core GitLab URL defaults, validation, normalization, and project/global inheritance.
- Expose project and global GitLab instance/API URL fields in settings with save-split handling.
- Cover configuration resolution, settings parity, and settings UI save behavior with tests.
- Document the new settings and add a changeset for the published CLI package.

Files changed:
 .changeset/fn-7422-gitlab-configuration.md         |  7 ++
 docs/settings-reference.md                         |  6 +-
 docs/task-management.md                            |  2 +-
 packages/core/src/__tests__/gitlab-config.test.ts  | 74 +++++++++++++++++++
 .../core/src/__tests__/settings-parity.test.ts     | 22 +++++-
 packages/core/src/gitlab-config.ts                 | 79 +++++++++++++++++++++
 packages/core/src/index.ts                         |  4 +-
 packages/core/src/settings-schema.ts               |  4 ++
 packages/core/src/types.ts                         | 20 ++++++
 .../app/__tests__/settings-save-split.test.ts      | 53 ++++++++++++++
 .../dashboard/app/components/SettingsModal.tsx     | 20 ++++++
 .../__tests__/SettingsModal.general.test.tsx       | 82 ++++++++++++++++++++++
 .../app/components/settings/save-split.ts          |  6 ++
 .../settings/sections/GeneralSection.tsx           | 15 ++++
 .../settings/sections/GlobalGeneralSection.tsx     | 14 ++++
 15 files changed, 402 insertions(+), 6 deletions(-)

Fusion-Task-Id: FN-7422

Fusion-Task-Lineage: 0110b4b6-71b0-4a62-afaf-61af65ebef03

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-02 11:38:33 -07:00

80 lines
3.0 KiB
TypeScript

import type { GlobalSettings, ProjectSettings } from "./types.js";
export const DEFAULT_GITLAB_INSTANCE_URL = "https://gitlab.com";
export const DEFAULT_GITLAB_API_BASE_URL = "https://gitlab.com/api/v4";
export interface GitlabConfigSettingsSource {
gitlabInstanceUrl?: string;
gitlabApiBaseUrl?: string;
}
export interface ResolveGitlabConfigInput {
project?: GitlabConfigSettingsSource | ProjectSettings | null;
global?: GitlabConfigSettingsSource | GlobalSettings | null;
}
export interface ResolvedGitlabConfig {
instanceUrl: string;
apiBaseUrl: string;
}
function readConfiguredString(value: unknown): string | undefined {
if (typeof value !== "string") return undefined;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
}
function normalizeHttpUrl(value: string, label: string): string {
let parsed: URL;
try {
parsed = new URL(value);
} catch {
throw new Error(`${label} must be a valid absolute http(s) URL`);
}
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
throw new Error(`${label} must use http:// or https://`);
}
if (parsed.username || parsed.password) {
throw new Error(`${label} must not include username or password userinfo`);
}
if (!parsed.hostname) {
throw new Error(`${label} must include a hostname`);
}
parsed.hash = "";
parsed.search = "";
parsed.pathname = parsed.pathname.replace(/\/+$/u, "") || "/";
const normalized = parsed.toString().replace(/\/$/u, "");
return normalized;
}
function deriveApiBaseUrl(instanceUrl: string): string {
const parsed = new URL(instanceUrl);
const basePath = parsed.pathname.replace(/\/+$/u, "");
parsed.pathname = `${basePath}/api/v4`.replace(/\/+/gu, "/");
parsed.search = "";
parsed.hash = "";
return parsed.toString().replace(/\/$/u, "");
}
/**
* FNXC:GitLabConfiguration 2026-07-02-00:00:
* FN-7422 only establishes typed GitLab.com and self-managed URL configuration for later GitLab auth/import/tracking subtasks. Normalize and validate here before any future network client consumes these settings, preserving self-managed path prefixes while rejecting non-http(s) URLs and userinfo-bearing URLs.
*/
export function resolveGitlabConfig(input: ResolveGitlabConfigInput = {}): ResolvedGitlabConfig {
const projectInstanceUrl = readConfiguredString(input.project?.gitlabInstanceUrl);
const globalInstanceUrl = readConfiguredString(input.global?.gitlabInstanceUrl);
const projectApiBaseUrl = readConfiguredString(input.project?.gitlabApiBaseUrl);
const globalApiBaseUrl = readConfiguredString(input.global?.gitlabApiBaseUrl);
const instanceUrl = normalizeHttpUrl(projectInstanceUrl ?? globalInstanceUrl ?? DEFAULT_GITLAB_INSTANCE_URL, "GitLab instance URL");
const apiBaseUrl = projectApiBaseUrl ?? globalApiBaseUrl
? normalizeHttpUrl(projectApiBaseUrl ?? globalApiBaseUrl ?? "", "GitLab API base URL")
: instanceUrl === DEFAULT_GITLAB_INSTANCE_URL
? DEFAULT_GITLAB_API_BASE_URL
: deriveApiBaseUrl(instanceUrl);
return { instanceUrl, apiBaseUrl };
}