mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
feat(forge): Phase 0 — git_provider column + registration-time forge validation (#569)
* feat(forge): Phase 0 — git_provider column + registration-time forge validation Pointing a project at a GitLab/Gitea git_url used to fail silently, several steps deep, at first PR. New pure policy module (foundation/policy/forge.py) detects the provider from the git_url host and validates at the ProjectService create/update chokepoint: github auto-detects and auto-stamps, explicit git_provider=github is the GitHub Enterprise escape hatch, gitlab/gitea are recognized-but-not-yet-supported, unknown hosts get a loud rejection with guidance. An update changing git_url does NOT inherit a stored auto-stamped provider (restating the override is required), so a host swap can't smuggle the escape hatch past validation. Migration 075 adds the nullable projects.git_provider column; the panel project dialogs show the detected forge. Phase 0 of the forge-providers spec. * fix(panel): mock-mode forge detection extracts the real host CodeQL js/incomplete-url-substring-sanitization: the substring check matched github.com anywhere in the URL. Extract the hostname (URL parse or scp-form regex, mirroring forge.py) and require an exact match. --------- Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
@@ -27,6 +27,7 @@ import { Team, type ProjectCreate } from "@/types";
|
||||
import { EnvironmentLadderEditor } from "@/components/projects/environment-ladder-editor";
|
||||
import { validateLadder } from "@/components/projects/ladder-validation";
|
||||
import { HelpTip } from "@/components/ui/help-tip";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
||||
const cells: { value: Team; label: string }[] = [
|
||||
{ value: Team.BACKEND, label: "Backend" },
|
||||
@@ -188,6 +189,19 @@ export function CreateProjectDialog() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Forge (read-only — GitHub-only today) */}
|
||||
<div className="grid gap-2">
|
||||
<HelpTip label="Auto-detected from the Git URL's host at creation; RoboCo's PR/CI/review surface is GitHub-only today. GitLab & Gitea support planned.">
|
||||
<Label>Forge</Label>
|
||||
</HelpTip>
|
||||
<div>
|
||||
<Badge variant="secondary">GitHub</Badge>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
GitLab & Gitea support planned.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Git Token */}
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="git_token" className="flex items-center gap-1">
|
||||
|
||||
@@ -30,6 +30,15 @@ import { Team, type ProjectUpdate, type Project } from "@/types";
|
||||
import { EnvironmentLadderEditor } from "@/components/projects/environment-ladder-editor";
|
||||
import { validateLadder } from "@/components/projects/ladder-validation";
|
||||
import { HelpTip } from "@/components/ui/help-tip";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
||||
// A null git_provider means "not yet stamped" (pre-Phase-0 project or a
|
||||
// non-github.com host awaiting an explicit choice) — RoboCo is GitHub-only
|
||||
// today either way, so the badge falls back to "GitHub" rather than "Unknown".
|
||||
function forgeLabel(gitProvider: string | null): string {
|
||||
if (!gitProvider) return "GitHub";
|
||||
return gitProvider.charAt(0).toUpperCase() + gitProvider.slice(1);
|
||||
}
|
||||
|
||||
const cells: { value: Team; label: string }[] = [
|
||||
{ value: Team.BACKEND, label: "Backend" },
|
||||
@@ -308,6 +317,19 @@ function EditProjectForm({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Forge (read-only — GitHub-only today) */}
|
||||
<div className="grid gap-2">
|
||||
<HelpTip label="Auto-detected from the Git URL's host; RoboCo's PR/CI/review surface is GitHub-only today. GitLab & Gitea support planned.">
|
||||
<Label>Forge</Label>
|
||||
</HelpTip>
|
||||
<div>
|
||||
<Badge variant="secondary">{forgeLabel(project.git_provider)}</Badge>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
GitLab & Gitea support planned.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Git Token Section */}
|
||||
<div className="grid gap-2 p-3 border rounded-lg bg-muted/30">
|
||||
<div className="flex items-center justify-between">
|
||||
|
||||
@@ -8,6 +8,26 @@ import type {
|
||||
} from "@/types";
|
||||
import { isMockMode } from "@/lib/mock-data";
|
||||
|
||||
// Mock-mode github.com detection: real host extraction (mirrors the backend's
|
||||
// forge.py `_extract_host`) instead of a raw substring match, so a URL like
|
||||
// "https://github.com.evil.tld/x/y.git" doesn't false-positive as github.
|
||||
const detectGithubProvider = (gitUrl: string): "github" | null => {
|
||||
const url = gitUrl.trim();
|
||||
let host: string | null = null;
|
||||
if (url.includes("://")) {
|
||||
try {
|
||||
host = new URL(url).hostname.toLowerCase() || null;
|
||||
} catch {
|
||||
host = null;
|
||||
}
|
||||
} else {
|
||||
// scp-like SSH syntax: [user@]host:path (e.g. git@github.com:owner/repo.git)
|
||||
const match = /^(?:[^@/]+@)?([^/:]+):/.exec(url);
|
||||
host = match ? match[1].toLowerCase() : null;
|
||||
}
|
||||
return host === "github.com" ? "github" : null;
|
||||
};
|
||||
|
||||
// Mock data for offline mode
|
||||
const mockProjects: Project[] = [
|
||||
{
|
||||
@@ -15,6 +35,7 @@ const mockProjects: Project[] = [
|
||||
name: "roboco",
|
||||
slug: "roboco",
|
||||
git_url: "https://github.com/rennf93/roboco.git",
|
||||
git_provider: "github",
|
||||
default_branch: "master",
|
||||
protected_branches: ["master", "slave"],
|
||||
assigned_cell: Team.BACKEND,
|
||||
@@ -33,6 +54,7 @@ const mockProjects: Project[] = [
|
||||
name: "roboco-website",
|
||||
slug: "roboco-website",
|
||||
git_url: "https://github.com/rennf93/roboco-website.git",
|
||||
git_provider: "github",
|
||||
default_branch: "master",
|
||||
protected_branches: ["master"],
|
||||
assigned_cell: Team.FRONTEND,
|
||||
@@ -124,6 +146,8 @@ export const projectsApi = {
|
||||
name: project.name,
|
||||
slug: project.slug,
|
||||
git_url: project.git_url,
|
||||
// Mock mode: mirror the backend's auto-detect (github.com -> github).
|
||||
git_provider: project.git_provider ?? detectGithubProvider(project.git_url),
|
||||
default_branch: project.default_branch ?? "main",
|
||||
environments: project.environments ?? null,
|
||||
protected_branches: project.protected_branches ?? ["main", "master"],
|
||||
|
||||
@@ -1033,6 +1033,9 @@ export interface Project {
|
||||
name: string;
|
||||
slug: string;
|
||||
git_url: string;
|
||||
// Forge provider ("github"|"gitlab"|"gitea"); null = auto-detect from
|
||||
// git_url host (github.com -> github, stamped on create). GitHub-only today.
|
||||
git_provider: string | null;
|
||||
default_branch: string;
|
||||
// Ordered environment ladder (first=head/PR-target, last=prod/release-target).
|
||||
// Null/empty => degenerate 1-rung ladder synthesized from default_branch.
|
||||
@@ -1071,6 +1074,8 @@ export interface ProjectCreate {
|
||||
name: string;
|
||||
slug: string;
|
||||
git_url: string;
|
||||
// null/omitted = auto-detect from git_url host (github.com -> github).
|
||||
git_provider?: string | null;
|
||||
default_branch?: string;
|
||||
// Ordered environment ladder; null/empty inherits default_branch (shim).
|
||||
environments?: EnvironmentRung[] | null;
|
||||
@@ -1089,6 +1094,8 @@ export interface ProjectCreate {
|
||||
export interface ProjectUpdate {
|
||||
name?: string;
|
||||
git_url?: string;
|
||||
// null = revert to auto-detect; omitted = leave unchanged.
|
||||
git_provider?: string | null;
|
||||
default_branch?: string;
|
||||
// Ordered environment ladder; null clears (reverts to default_branch shim).
|
||||
environments?: EnvironmentRung[] | null;
|
||||
|
||||
Reference in New Issue
Block a user