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

  1. Enter the repository as 'owner/repo' (e.g. 'actualbudget/themes') in the theme installer input
  2. Trim input and reject empty strings at the UI layer before calling normalizeGitHubRepo
  3. If accepting full URLs, strip the 'https://github.com/' prefix before validation
  4. 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

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


AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29). Data as JSON: /api/errors/d3fcf44e065a0b83. Report an issue: GitHub.