feat(FN-3868): add changeset for github tracking settings

Adds a changeset for the GitHub tracking settings feature (FN-3868), preparing it for the next release. The feature itself was implemented in earlier steps; this changeset marks it for publication to npm.

Fusion-Task-Id: FN-3868
This commit is contained in:
Fusion
2026-05-09 12:07:05 -07:00
committed by gsxdsm
parent ac0606daaf
commit 4b6a14974b
9 changed files with 244 additions and 3 deletions

View File

@@ -69,6 +69,23 @@ describe("settings key parity", () => {
expect(DEFAULT_PROJECT_SETTINGS.completionDocumentationMode).toBe("off");
});
it("keeps github tracking keys in expected scopes with documented defaults", () => {
expect(DEFAULT_PROJECT_SETTINGS.githubTrackingEnabledByDefault).toBe(false);
expect(DEFAULT_PROJECT_SETTINGS.githubTrackingDefaultRepo).toBeUndefined();
expect(DEFAULT_PROJECT_SETTINGS.githubAuthMode).toBe("gh-cli");
expect(DEFAULT_PROJECT_SETTINGS.githubAuthToken).toBeUndefined();
expect(DEFAULT_GLOBAL_SETTINGS.githubTrackingDefaultRepo).toBeUndefined();
expect(isProjectSettingsKey("githubTrackingEnabledByDefault")).toBe(true);
expect(isGlobalSettingsKey("githubTrackingEnabledByDefault")).toBe(false);
expect(isProjectSettingsKey("githubAuthMode")).toBe(true);
expect(isGlobalSettingsKey("githubAuthMode")).toBe(false);
expect(isProjectSettingsKey("githubAuthToken")).toBe(true);
expect(isGlobalSettingsKey("githubAuthToken")).toBe(false);
expect(isProjectSettingsKey("githubTrackingDefaultRepo")).toBe(true);
expect(isGlobalSettingsKey("githubTrackingDefaultRepo")).toBe(true);
});
it("keeps remoteAccess scoped to global settings only", () => {
const globalKeys = GLOBAL_SETTINGS_KEYS as readonly string[];
const projectKeys = PROJECT_SETTINGS_KEYS as readonly string[];
@@ -89,10 +106,10 @@ describe("settings key parity", () => {
expect((DEFAULT_PROJECT_SETTINGS as Record<string, unknown>).experimentalFeatures).toBeUndefined();
});
it("No key appears in both GLOBAL_SETTINGS_KEYS and PROJECT_SETTINGS_KEYS", () => {
it("only intentional shared keys appear in both global and project scopes", () => {
const projectKeySet = new Set(PROJECT_SETTINGS_KEYS as readonly string[]);
const overlap = (GLOBAL_SETTINGS_KEYS as readonly string[]).filter((key) => projectKeySet.has(key));
expect(overlap).toEqual([]);
expect(overlap).toEqual(["githubTrackingDefaultRepo"]);
});
});

View File

@@ -0,0 +1,54 @@
import { describe, expect, it } from "vitest";
import {
validateGithubAuthMode,
validateGithubRepoSlug,
validateUnavailableNodePolicy,
} from "../settings-validation.js";
describe("settings-validation", () => {
describe("validateUnavailableNodePolicy", () => {
it("accepts supported policies", () => {
expect(validateUnavailableNodePolicy("block")).toBe("block");
expect(validateUnavailableNodePolicy("fallback-local")).toBe("fallback-local");
});
it("returns undefined for invalid values", () => {
expect(validateUnavailableNodePolicy("fallback")).toBeUndefined();
expect(validateUnavailableNodePolicy(123)).toBeUndefined();
expect(validateUnavailableNodePolicy(undefined)).toBeUndefined();
});
});
describe("validateGithubAuthMode", () => {
it("accepts supported auth modes", () => {
expect(validateGithubAuthMode("gh-cli")).toBe("gh-cli");
expect(validateGithubAuthMode("token")).toBe("token");
});
it("returns undefined for invalid values", () => {
expect(validateGithubAuthMode("oauth")).toBeUndefined();
expect(validateGithubAuthMode(123)).toBeUndefined();
expect(validateGithubAuthMode(undefined)).toBeUndefined();
});
});
describe("validateGithubRepoSlug", () => {
it("accepts valid owner/repo slugs", () => {
expect(validateGithubRepoSlug("owner/repo")).toBe("owner/repo");
expect(validateGithubRepoSlug("Owner.Name/repo_name-1")).toBe("Owner.Name/repo_name-1");
});
it("treats empty strings as unset", () => {
expect(validateGithubRepoSlug("")).toBeUndefined();
expect(validateGithubRepoSlug(" ")).toBeUndefined();
});
it("returns undefined for malformed slugs and invalid types", () => {
expect(validateGithubRepoSlug("owner")).toBeUndefined();
expect(validateGithubRepoSlug("owner/repo/extra")).toBeUndefined();
expect(validateGithubRepoSlug("owner repo/repo")).toBeUndefined();
expect(validateGithubRepoSlug(42)).toBeUndefined();
expect(validateGithubRepoSlug(undefined)).toBeUndefined();
});
});
});

View File

@@ -52,6 +52,7 @@ export const DEFAULT_GLOBAL_SETTINGS = {
updateCheckFrequency: "daily",
autoReloadOnVersionChange: true,
showGitHubStarButton: true,
githubTrackingDefaultRepo: undefined,
modelOnboardingComplete: undefined,
useClaudeCli: undefined,
useDroidCli: undefined,
@@ -229,6 +230,10 @@ export const DEFAULT_PROJECT_SETTINGS = {
autoUpdatePrStatus: false,
githubCommentOnDone: false,
githubCommentTemplate: undefined,
githubTrackingEnabledByDefault: false,
githubTrackingDefaultRepo: undefined,
githubAuthMode: "gh-cli",
githubAuthToken: undefined,
autoBackupEnabled: false,
autoBackupSchedule: "0 2 * * *",
autoBackupRetention: 7,

View File

@@ -1,6 +1,8 @@
import type { UnavailableNodePolicy } from "./types.js";
import type { GithubAuthMode, UnavailableNodePolicy } from "./types.js";
const UNAVAILABLE_NODE_POLICIES: readonly UnavailableNodePolicy[] = ["block", "fallback-local"] as const;
const GITHUB_AUTH_MODES: readonly GithubAuthMode[] = ["gh-cli", "token"] as const;
const GITHUB_REPO_SLUG_PATTERN = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
/**
* Validates a project unavailable-node routing policy value.
@@ -18,3 +20,29 @@ export function validateUnavailableNodePolicy(value: unknown): UnavailableNodePo
? (value as UnavailableNodePolicy)
: undefined;
}
/** Returns a validated GitHub auth mode for project settings, otherwise undefined. */
export function validateGithubAuthMode(value: unknown): GithubAuthMode | undefined {
if (value === undefined) {
return undefined;
}
if (typeof value !== "string") {
return undefined;
}
return (GITHUB_AUTH_MODES as readonly string[]).includes(value) ? (value as GithubAuthMode) : undefined;
}
/** Returns a validated owner/repo GitHub slug, otherwise undefined. Empty string is treated as unset. */
export function validateGithubRepoSlug(value: unknown): string | undefined {
if (value === undefined) {
return undefined;
}
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
if (trimmed.length === 0) {
return undefined;
}
return GITHUB_REPO_SLUG_PATTERN.test(trimmed) ? trimmed : undefined;
}

View File

@@ -1616,6 +1616,9 @@ export interface GlobalSettings {
* header. Defaults to true (visible). The button is also hidden once the
* user has clicked it (tracked client-side in localStorage). */
showGitHubStarButton?: boolean;
/** Global fallback GitHub tracking repo in `owner/repo` format (FN-3868).
* Used when a project has no githubTrackingDefaultRepo. */
githubTrackingDefaultRepo?: string;
/** Cadence for automatic update checks. The dashboard's `/update-check`
* route uses this to decide whether to consult npm or return a cached
* result.
@@ -1849,6 +1852,9 @@ export interface RemoteAccessProjectSettings {
lifecycle: RemoteAccessLifecycleConfig;
}
/** GitHub authentication strategy used by project issue-tracking settings (FN-3868). */
export type GithubAuthMode = "gh-cli" | "token";
/**
* Project-level settings stored in `.fusion/config.json`.
*
@@ -2187,6 +2193,18 @@ export interface ProjectSettings {
/** Optional template used for GitHub issue comments posted on task completion.
* Supports `{taskId}` and `{taskTitle}` placeholders. */
githubCommentTemplate?: string;
/** When true, new tasks default GitHub tracking to enabled for this project (FN-3868).
* Default: false. */
githubTrackingEnabledByDefault?: boolean;
/** Project default GitHub tracking repo in `owner/repo` format (FN-3868).
* Falls back to global githubTrackingDefaultRepo when unset. */
githubTrackingDefaultRepo?: string;
/** GitHub auth strategy for issue-tracking API calls in this project (FN-3868).
* Default: "gh-cli". */
githubAuthMode?: GithubAuthMode;
/** Personal access token used when githubAuthMode is "token" (FN-3868).
* Stored as a plain settings string in this phase. */
githubAuthToken?: string;
/** When true, automatic database backups are enabled. Default: false. */
autoBackupEnabled?: boolean;
/** Cron expression for backup schedule. Default: "0 2 * * *" (daily at 2 AM). */