actualbudget/actual · error
Invalid repo: must be in "owner/repo" format
Error message
Invalid repo: must be in "owner/repo" format
What it means
normalizeGitHubRepo validates a user-supplied GitHub repo string: after trimming, it must contain a '/'. If not, it throws 'Invalid repo: must be in "owner/repo" format'. This is the first of two validation checks that turn free-text input into a canonical https://github.com/owner/repo URL for custom theme installation.
Source
Thrown at packages/desktop-client/src/style/customThemes.ts:45
export function extractRepoOwner(repo: string): string {
if (!repo || typeof repo !== 'string' || !repo.includes('/')) {
return 'Unknown';
}
const parts = repo.split('/');
const owner = parts[0]?.trim();
return owner || 'Unknown';
}
/**
* Normalize a GitHub repo identifier to a full GitHub URL.
* Accepts "owner/repo" format.
* Returns "https://github.com/owner/repo".
* @throws {Error} If repo is invalid or missing owner/repo.
*/
export function normalizeGitHubRepo(repo: string): string {
const trimmed = repo.trim();
if (!trimmed.includes('/')) {
throw new Error('Invalid repo: must be in "owner/repo" format');
}
const parts = trimmed.split('/');
const owner = parts[0]?.trim();
const repoName = parts[1]?.trim();
if (!owner || !repoName) {
throw new Error('Invalid repo: must include both owner and repo name');
}
return `https://github.com/${owner}/${repoName}`;
}
/**
* Try fetching actual.css from main branch.
*/
export function fetchThemeCss(repo: string): Promise<string> {
const url = new URL(View on GitHub (pinned to d4334cb6e6)
Solutions
- Enter the repository as 'owner/repo' (e.g. 'actualbudget/themes') in the theme installer input
- Trim input and reject empty strings at the UI layer before calling normalizeGitHubRepo
- If accepting full URLs, strip the 'https://github.com/' prefix before validation
- Improve the UI with a placeholder and inline validation to enforce the owner/repo shape
Example fix
// before
normalizeGitHubRepo('my-theme'); // throws
// after
const input = 'my-theme';
const repo = input.includes('/') ? input : `actualbudget/${input}`;
normalizeGitHubRepo(repo); // 'https://github.com/actualbudget/my-theme' Defensive patterns
Strategy: validation
Validate before calling
const parseRepoInput = (input: string): string | null => {
const cleaned = input.trim()
.replace(/^https?:\/\/github\.com\//i, '')
.replace(/^\/+|\/+$/g, '');
return /^[\w.-]+\/[\w.-]+$/.test(cleaned) ? cleaned : null;
};
// call normalizeGitHubRepo only when parseRepoInput returns non-null Type guard
const isOwnerRepo = (s: string): s is `${string}/${string}` =>
s.includes('/') && s.trim().split('/').every(p => p.trim().length > 0); Try / catch
try {
const url = normalizeGitHubRepo(userInput);
} catch (err) {
if (err.message.includes('Invalid repo')) {
setFieldError('Repository must look like owner/repo');
} else throw err;
} Prevention
- Validate the owner/repo shape inline in the form before submission
- Accept and normalize full GitHub URLs by stripping the prefix first
- Trim and strip stray slashes from pasted input
- Add unit tests for edge inputs: empty, 'name', '/name', 'name/'
When it happens
Trigger: Calling normalizeGitHubRepo (directly or via ThemeInstaller) with a bare repo name like 'my-theme', a full URL like 'https://github.com/owner/repo' (contains slashes so passes, but 'github.com owner repo' spaced input trimmed without slash fails), or an empty/whitespace string.
Common situations: Users pasting just the theme name; typing the repo without the owner; leaving the field blank and submitting; input fields that strip the slash.
Related errors
- Invalid repo: must include both owner and repo name
- Cleanup group name cannot be empty
- Invalid --name: must be a non-empty string.
- No update fields provided. Use --name or --offbudget.
- Invalid cutoff date: expected a valid date (e.g. YYYY-MM-DD)
AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29).
Data as JSON: /api/errors/d3fcf44e065a0b83.
Report an issue: GitHub.