Files
fusion/packages/core/src/gitlab-config.ts
gsxdsm f9733341b5 FN-7453: add GitLab enablement controls
Add a settings-controlled GitLab enablement path while preserving saved GitLab configuration.

- Add project and global GitLab enable settings with collapsible Settings UI for URL and token controls.
- Gate GitLab auth, import loading, and import actions when GitLab integration is disabled.
- Preserve global GitLab values separately during global settings saves and document the new behavior.
- Cover enablement resolution, settings save behavior, disabled import UI, and GitLab route/auth gating with tests.

Files changed:
 .changeset/fn-7453-gitlab-enable-disclosure.md     |  7 ++
 docs/dashboard-guide.md                            |  7 ++
 docs/gitlab-parity-inventory.md                    |  1 +
 docs/settings-reference.md                         |  4 ++
 docs/task-management.md                            |  6 +-
 packages/core/src/__tests__/gitlab-config.test.ts  | 17 ++++-
 .../core/src/__tests__/settings-parity.test.ts     | 18 +++--
 packages/core/src/gitlab-config.ts                 | 15 ++++-
 packages/core/src/index.ts                         |  2 +-
 packages/core/src/settings-schema.ts               |  2 +
 packages/core/src/types.ts                         |  5 ++
 .../app/__tests__/settings-save-split.test.ts      | 20 +++---
 .../dashboard/app/components/GitHubImportModal.tsx | 43 +++++++++---
 .../dashboard/app/components/SettingsModal.css     | 77 +++++++++++++++++++++
 .../dashboard/app/components/SettingsModal.tsx     | 34 ++++++++-
 .../__tests__/GitHubImportModal.test.tsx           | 19 ++++++
 .../__tests__/SettingsModal.general.test.tsx       | 78 ++++++++++++++++++++++
 .../SettingsModal.scheduling-merge.test.tsx        |  7 +-
 .../app/components/settings/save-split.ts          |  5 +-
 .../settings/sections/GeneralSection.tsx           | 36 ++++++----
 .../settings/sections/GlobalGeneralSection.tsx     | 71 ++++++++++++--------
 .../components/settings/sections/MergeSection.tsx  | 44 +++++++-----
 .../dashboard/src/__tests__/gitlab-auth.test.ts    | 20 ++++++
 .../dashboard/src/__tests__/routes-gitlab.test.ts  | 11 +++
 packages/dashboard/src/gitlab-auth.ts              | 11 ++-
 25 files changed, 458 insertions(+), 102 deletions(-)

Fusion-Task-Id: FN-7453

Fusion-Task-Lineage: 81fbd39d-675f-49d6-8d13-b7fcb4d25658

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-02 19:01:09 -07:00

93 lines
3.7 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 {
gitlabEnabled?: boolean;
gitlabInstanceUrl?: string;
gitlabApiBaseUrl?: string;
}
export interface ResolveGitlabConfigInput {
project?: GitlabConfigSettingsSource | ProjectSettings | null;
global?: GitlabConfigSettingsSource | GlobalSettings | null;
}
export interface ResolvedGitlabConfig {
enabled: boolean;
instanceUrl: string;
apiBaseUrl: string;
}
export function resolveGitlabEnabled(input: ResolveGitlabConfigInput = {}): boolean {
/*
FNXC:GitLabEnablement 2026-07-02-00:00:
FN-7453 separates saved GitLab URL/token configuration from whether GitLab integrations are active. Undefined remains effectively enabled for backward compatibility; explicit project false overrides global true/undefined and short-circuits runtime network paths before URL or token validation.
*/
if (typeof input.project?.gitlabEnabled === "boolean") return input.project.gitlabEnabled;
if (typeof input.global?.gitlabEnabled === "boolean") return input.global.gitlabEnabled;
return true;
}
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 enabled = resolveGitlabEnabled(input);
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 { enabled, instanceUrl, apiBaseUrl };
}