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:
5
.changeset/FN-3868-github-tracking-settings.md
Normal file
5
.changeset/FN-3868-github-tracking-settings.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
Add global and project settings for GitHub issue tracking: global default tracking repo, project-level default tracking repo, per-project tracking toggle for new tasks, GitHub auth mode (`gh-cli` | `token`), and optional stored personal access token. This is foundational settings work for FN-3868 → FN-3876; behavior wiring ships in downstream subtasks.
|
||||
@@ -59,6 +59,7 @@ In **Settings → Notifications**, use **Test message notification** to exercise
|
||||
| `openrouterModelSync` | `boolean` | `true` | Sync OpenRouter model catalog into model pickers at startup. |
|
||||
| `opencodeGoModelSync` | `boolean` | `true` | Sync opencode-go model catalog at startup via `opencode models opencode --refresh`, normalizing discovered `opencode/...` IDs into the `opencode-go` provider surface used by `/api/models`. |
|
||||
| `updateCheckEnabled` | `boolean` | `true` | When enabled, Fusion performs a daily npm registry check for new `@runfusion/fusion` versions and shows update notices in CLI/dashboard. |
|
||||
| `githubTrackingDefaultRepo` | `string` | `undefined` | Global fallback issue-tracking repo (`owner/repo`) introduced in FN-3868 as groundwork for the FN-3868 → FN-3876 GitHub tracking epic; behavior wiring lands in downstream subtasks. |
|
||||
| `autoReloadOnVersionChange` | `boolean` | `true` | When enabled (default), the dashboard automatically reloads when a new build version is detected via `/version.json` polling or service worker activation. Set to `false` to suppress automatic reloads — the user must manually refresh to pick up updates. |
|
||||
| `modelOnboardingComplete` | `boolean` | `undefined` | Whether AI onboarding has been completed or dismissed. |
|
||||
| `executionGlobalProvider` | `string` | `undefined` | Global baseline provider for task execution. Project `executionProvider` overrides this. |
|
||||
@@ -229,6 +230,10 @@ Defaults from `DEFAULT_PROJECT_SETTINGS`; key scope from `PROJECT_SETTINGS_KEYS`
|
||||
| `autoUpdatePrStatus` | `boolean` | `false` | Auto-refresh PR status badges. |
|
||||
| `githubCommentOnDone` | `boolean` | `false` | When enabled, tasks imported from GitHub issues post a completion comment to the source issue when the task moves to `done`. |
|
||||
| `githubCommentTemplate` | `string` | `undefined` | Optional issue comment template used by `githubCommentOnDone`. Supports `{taskId}` and `{taskTitle}` placeholders. If unset, Fusion uses a default completion message. |
|
||||
| `githubTrackingEnabledByDefault` | `boolean` | `false` | Project-level default for enabling issue tracking on new tasks. Added in FN-3868 as foundation for the FN-3868 → FN-3876 GitHub tracking epic; task/issue behavior ships in later subtasks. |
|
||||
| `githubTrackingDefaultRepo` | `string` | `undefined` | Project default issue-tracking repo (`owner/repo`). Added in FN-3868 as foundation for the FN-3868 → FN-3876 epic; downstream subtasks implement runtime usage. |
|
||||
| `githubAuthMode` | `"gh-cli" \| "token"` | `"gh-cli"` | Project GitHub auth strategy selector added in FN-3868 for the FN-3868 → FN-3876 tracking epic; auth wiring is deferred to later subtasks. |
|
||||
| `githubAuthToken` | `string` | `undefined` | Optional project PAT used when `githubAuthMode` is `"token"`. Added in FN-3868 as data-layer groundwork; downstream subtasks consume it. |
|
||||
| `autoCreatePr` | `boolean` | `false` | Auto-create PRs for completed tasks. |
|
||||
| `autoBackupEnabled` | `boolean` | `false` | Enable scheduled DB backups. |
|
||||
| `autoBackupSchedule` | `string` | `"0 2 * * *"` | Backup cron schedule. |
|
||||
|
||||
@@ -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"]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
54
packages/core/src/__tests__/settings-validation.test.ts
Normal file
54
packages/core/src/__tests__/settings-validation.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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). */
|
||||
|
||||
@@ -1600,6 +1600,8 @@ export function SettingsModal({
|
||||
...form,
|
||||
worktreeInitCommand: form.worktreeInitCommand?.trim() || undefined,
|
||||
taskPrefix: form.taskPrefix?.trim() || undefined,
|
||||
githubTrackingDefaultRepo: form.githubTrackingDefaultRepo?.trim() || undefined,
|
||||
githubAuthToken: form.githubAuthToken?.trim() || undefined,
|
||||
overlapIgnorePaths: (form.overlapIgnorePaths ?? []).map((path) => path.trim()).filter((path) => path.length > 0),
|
||||
experimentalFeatures: normalizeExperimentalFeaturesForSave(form.experimentalFeatures),
|
||||
};
|
||||
@@ -1988,6 +1990,20 @@ export function SettingsModal({
|
||||
it hidden even before clicking.
|
||||
</small>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="globalGithubTrackingDefaultRepo">Global default tracking repo</label>
|
||||
<input
|
||||
id="globalGithubTrackingDefaultRepo"
|
||||
type="text"
|
||||
className="input"
|
||||
placeholder="owner/repo"
|
||||
value={form.githubTrackingDefaultRepo ?? ""}
|
||||
onChange={(e) =>
|
||||
setForm((f) => ({ ...f, githubTrackingDefaultRepo: e.target.value || undefined }))
|
||||
}
|
||||
/>
|
||||
<small>Projects inherit this value when they do not set a project default tracking repo.</small>
|
||||
</div>
|
||||
<CliBinaryPanel />
|
||||
<div className="form-group">
|
||||
<label htmlFor="persistAgentToolOutput" className="checkbox-label">
|
||||
@@ -3563,6 +3579,61 @@ export function SettingsModal({
|
||||
</details>
|
||||
</div>
|
||||
)}
|
||||
<h4 className="settings-section-heading settings-section-heading--spaced">GitHub Issue Tracking</h4>
|
||||
<div className="form-group">
|
||||
<label htmlFor="githubTrackingEnabledByDefault" className="checkbox-label">
|
||||
<input
|
||||
id="githubTrackingEnabledByDefault"
|
||||
type="checkbox"
|
||||
checked={form.githubTrackingEnabledByDefault ?? false}
|
||||
onChange={(e) =>
|
||||
setForm((f) => ({ ...f, githubTrackingEnabledByDefault: e.target.checked }))
|
||||
}
|
||||
/>
|
||||
Default GitHub tracking ON for new tasks
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="projectGithubTrackingDefaultRepo">Project default tracking repo</label>
|
||||
<input
|
||||
id="projectGithubTrackingDefaultRepo"
|
||||
type="text"
|
||||
className="input"
|
||||
placeholder="owner/repo"
|
||||
value={form.githubTrackingDefaultRepo ?? ""}
|
||||
onChange={(e) =>
|
||||
setForm((f) => ({ ...f, githubTrackingDefaultRepo: e.target.value || undefined }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="githubAuthMode">GitHub auth mode</label>
|
||||
<select
|
||||
id="githubAuthMode"
|
||||
className="select"
|
||||
value={form.githubAuthMode ?? "gh-cli"}
|
||||
onChange={(e) =>
|
||||
setForm((f) => ({ ...f, githubAuthMode: e.target.value as "gh-cli" | "token" }))
|
||||
}
|
||||
>
|
||||
<option value="gh-cli">GitHub CLI (gh auth)</option>
|
||||
<option value="token">Personal access token</option>
|
||||
</select>
|
||||
</div>
|
||||
{(form.githubAuthMode ?? "gh-cli") === "token" && (
|
||||
<div className="form-group">
|
||||
<label htmlFor="githubAuthToken">GitHub personal access token</label>
|
||||
<input
|
||||
id="githubAuthToken"
|
||||
type="password"
|
||||
className="input"
|
||||
value={form.githubAuthToken ?? ""}
|
||||
onChange={(e) =>
|
||||
setForm((f) => ({ ...f, githubAuthToken: e.target.value || undefined }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="form-group">
|
||||
<label htmlFor="includeTaskIdInCommit" className="checkbox-label">
|
||||
<input
|
||||
|
||||
@@ -530,6 +530,15 @@ describe("SettingsModal", () => {
|
||||
expect(projectPayload.persistAgentToolOutput).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it("renders global default tracking repo control", async () => {
|
||||
renderModal({ initialSection: "global-general" });
|
||||
await waitForSettingsModalReady();
|
||||
|
||||
const input = screen.getByLabelText("Global default tracking repo") as HTMLInputElement;
|
||||
expect(input.value).toBe("");
|
||||
expect(screen.getByText(/Projects inherit this value when they do not set a project default tracking repo/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Project General", () => {
|
||||
@@ -1855,6 +1864,35 @@ describe("SettingsModal", () => {
|
||||
expect(payload.pushAfterMerge).toBe(true);
|
||||
expect(payload.pushRemote).toBe("upstream main");
|
||||
});
|
||||
|
||||
it("renders and saves github issue tracking controls", async () => {
|
||||
renderModal({ initialSection: "merge" });
|
||||
await waitForSettingsModalReady();
|
||||
|
||||
expect(screen.getByRole("heading", { name: "GitHub Issue Tracking" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("checkbox", { name: "Default GitHub tracking ON for new tasks" })).not.toBeChecked();
|
||||
expect(screen.getByLabelText("Project default tracking repo")).toBeInTheDocument();
|
||||
|
||||
const authModeSelect = screen.getByLabelText("GitHub auth mode") as HTMLSelectElement;
|
||||
expect(authModeSelect.value).toBe("gh-cli");
|
||||
expect(screen.queryByLabelText("GitHub personal access token")).not.toBeInTheDocument();
|
||||
|
||||
await userEvent.click(screen.getByRole("checkbox", { name: "Default GitHub tracking ON for new tasks" }));
|
||||
await userEvent.type(screen.getByLabelText("Project default tracking repo"), "octo/repo");
|
||||
await userEvent.selectOptions(authModeSelect, "token");
|
||||
await userEvent.type(screen.getByLabelText("GitHub personal access token"), "ghp_test_token");
|
||||
await userEvent.click(screen.getByRole("button", { name: "Save" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateSettings).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const payload = mockUpdateSettings.mock.calls[0][0] as Record<string, unknown>;
|
||||
expect(payload.githubTrackingEnabledByDefault).toBe(true);
|
||||
expect(payload.githubTrackingDefaultRepo).toBe("octo/repo");
|
||||
expect(payload.githubAuthMode).toBe("token");
|
||||
expect(payload.githubAuthToken).toBe("ghp_test_token");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Experimental Features section", () => {
|
||||
|
||||
Reference in New Issue
Block a user