Steven
Steven5 хв читання

Одна кодова база, два застосунки: як зробити white-label без форку

GeekBye і Pavleur — це два по-різному брендовані десктопні застосунки, зібрані з одного репозиторію: без форку, без дубльованої кодової бази. Ось механіка часу збірки, яка перетворює одну кодову базу на два продукти, і однорядковий баг, через який наш другий застосунок представлявся неправильним іменем.

Інженерія
Архітектура
Збірка
Релізи GeekBye
Одна кодова база, два застосунки: як зробити white-label без форку

GeekBye — це нотатник для зустрічей. Pavleur — це AI-застосунок для тренування співбесід. Вони виглядають як два різні продукти, бо вони і є два різні продукти — але зібрані з однієї кодової бази, без форку й без гілки, що розповзається. Це інженерний бік white-labeling: не «навіщо продавати брендований застосунок» (це інший пост), а як ти насправді компілюєш один репозиторій у два застосунки — і непоказний баг, що ховається в кожній white-label системі, доки ти не почнеш його шукати.

Генеруй, а не гілкуй

Хибний спосіб запускати два продукти з однієї кодової бази — розсипати if (product === 'pavleur') по всьому застосунку або форкнути й молитися, щоб дві копії ніколи не розійшлися. Обидва підходи швидко гниють.

Підхід GeekBye — це генерація коду під час збірки. Кожен продукт має свій конфіг-файл — configs/geekbye/config.ts, configs/pavleur/config.ts — що зберігає його ідентичність: ім'я, bundle id, URL-протокол, репозиторій релізів для авто-оновлень тощо. Перед збіркою запускається один скрипт:

node scripts/build-product.js pavleur

Цей скрипт читає конфіг обраного продукту й робить три конкретні речі:

  1. Генерує два config-модулі — один для рендерера (src/config/product.ts), один для головного процесу (electron/config/product.ts) — кожен зі штампом заголовка «DO NOT EDIT MANUALLY — changes will be overwritten». Решта застосунку просто імпортує productName, applicationId та protocol з config і ніколи не задумується, який це бренд.
  2. Копіює ресурси лише цього бренду — іконки, маленьке лого, іконку toast-сповіщень. Збірка GeekBye фізично не містить жодного арту Pavleur і навпаки. Нічому нема витікати.
  3. Переписує package.json (на релізних збірках) — app id, ім'я продукту, URL-протокол, GitHub-репозиторій релізів для публікації і, що приємно, брендований текст дозволу на доступ до мікрофона, тож macOS питає «Pavleur хоче отримати доступ до мікрофона», а не неправильний бренд.

Весь застосунок бренд-агностичний; бренд впорскується під час збірки. Немає рантайм-перемикача продукту, який можна зіпсувати, бо в рантаймі завжди запечено лише один продукт.

Один backend, два бренди

Обидва застосунки звертаються до того самого backend. То як же сервер знає, якому продукту — і яким цінам, промптам та лімітам — належить запит?

Два сигнали, обидва задані під час збірки. Перший — application id (geekbye або pavleur), запечений у config і надісланий із запитами, тож backend може маршрутизувати до поведінки правильного продукту. Другий — User-Agent, який клієнт додає до кожного виклику backend, виду Product/version (platform) — наприклад, Pavleur/1.2.5 (win32) чи GeekBye/1.7.3 (darwin). Цей один заголовок безкоштовно дає backend-у атрибуцію по продукту й по ОС, без окремого поля аналітики. А авто-оновлення тим часом лишаються ізольованими: кожен продукт публікується у власний репозиторій релізів, тож оновлення GeekBye ніколи не може бути подане на інсталяцію Pavleur.

Онбординг теж різниться від продукту — майстер GeekBye про транскрипцію зустрічей, а Pavleur — про тренування співбесід — і потрібний обирається в рантаймі з єдиного запеченого application id. Та сама механіка, двоє вхідних дверей.

Баг: коли Pavleur називав себе GeekBye

Ось бойова історія, і це та сама, версія якої є в кожної white-label системи.

Генерація конфігу працювала. Іконки мінялися. Запит на мікрофон був брендований. Протоколи й bundle id були правильні. За будь-якою видимою міркою збірка Pavleur була Pavleur. І все ж якийсь час кожна збірка Pavleur дзвонила додому на backend, називаючи себе GeekBye.

Винуватцем був єдиний захардкоджений рядок, і ховався він у найменш показному місці, яке лишень можна уявити: білдер User-Agent, засунутий усередину мережевого хелпера повторних спроб. Усе, що дивиться на користувача, було проведене через config, але ця одна утиліта глибокої сантехніки досі мала в собі літерал `GeekBye/${version}`. Ніхто не думає про утиліту повторних спроб як про «брендинг», тож ніхто її й не перевіряв. Виправлення було на чотири рядки — імпортувати динамічне productName з config і підставити його замість захардкодженого бренду — але урок і є вся суть:

White-labeling — це не «чи змінилося лого». Це «чи проходить кожен рядок ідентичності через config». І останні недобитки ніколи не живуть в UI, де ти б їх шукав. Вони живуть у User-Agent, обробнику протоколу deep-link, тексті дозволу-запиту, повідомленнях про помилки — у сантехніці, яку ніхто не заносить у розділ «бренд». Єдиний надійний захист — ставитися до літерала бренду як до чогось, на що ти активно полюєш: грепни кодову базу на захардкоджене ім'я й зроби так, щоб збірка падала, якщо воно з'являється будь-де поза config, який має його визначати.

Три речі, яких нас навчив white-labeling

  1. Генеруй, а не гілкуй. Кодогенерація під час збірки двох крихітних config-модулів плюс вибіркове копіювання ресурсів б'є і форк, і рантайм-хащу перевірок продукту. Застосунок лишається бренд-агностичним; бренд — це вхідні дані збірки.
  2. Рядки ідентичності протікають у сантехніку. UI — це легкі 90%. Складні 10% — це User-Agent, URL-протокол, рядок дозволу на мікрофон, текст помилок. Проведи їх усі через один експорт productName / applicationId — і грепай сире ім'я бренду як CI-гейт, щоб випадковий літерал ніколи не потрапив у реліз.
  3. Один backend, багато брендів, розрізняй за id та заголовком. Заданий під час збірки application id плюс User-Agent виду Product/version (platform) дають атрибуцію по продукту й по платформі з нульовою дубльованою інфраструктурою — чисто відокремлюючи незмінну ідентичність часу збірки від рантайм-поведінки.

Про продуктовий і приватний бік запуску white-label нотатника під власним брендом дивись white-label AI-нотатник. Це шостий розділ історії v1, яка стає GeekBye v2 — про попередній розділ читай, чим насправді є реліз зі 127 commitів (v1.7.0); а про всю дугу — анатомію доставки програмного забезпечення до досконалості.

Схожі статті

Одна CSS-змінна, п'ять раундів рев'ю та ланцюжок інструментів Swift, що збрехав
Steven
Steven6 хв читання

Одна CSS-змінна, п'ять раундів рев'ю та ланцюжок інструментів Swift, що збрехав

GeekBye v2.0.7 зробив увесь оверлей регульовано напівпрозорим — що звучить як однорядкова зміна CSS і абсолютно нею не було. Справжня історія — про те, що виловило код-рев'ю: дві поверхні, які відмовлялися згасати, панель запису, що виглядала неправильно за тієї самої прозорості, і скомпільований бінарник, який стрибнув на 672 байти, бо наша власна документація підказала рецензенту неправильну версію Swift.

Інженерія
Дизайн
Збірка
Ми видалили 5 000 рядків аудіокоду — і транскрипти почали з'являтися двічі
Steven
Steven5 хв читання

Ми видалили 5 000 рядків аудіокоду — і транскрипти почали з'являтися двічі

GeekBye v1.6 вирвав два вбудовані Swift-транскрайбери й замінив їх одним уніфікованим конвеєром — чиста різниця понад 5 000 видалених рядків, зроблено просто тоді, коли люди були посеред зустрічі в застосунку. А потім транскрипти почали з'являтися двічі, бо баг переїхав на той єдиний шар, який досі вважав, що двигунів два.

Інженерія
Транскрипція
Архітектура
Що насправді являє собою реліз із 127 комітів
Steven
Steven5 хв читання

Що насправді являє собою реліз із 127 комітів

GeekBye v1.7.0 — це 127 комітів за одинадцять днів. Ззовні це схоже на сотню дрібниць. Зсередини це були дві великі функції, переплетені разом, — і одну з них спершу побудували не в тому місці, потім вирвали й перебудували посеред релізу. Ось анатомія великого релізу.

Інженерія
Реліз
Архітектура