JavaRush /Курсы /Swift SELF /Финальная упаковка CLI

Финальная упаковка CLI

Swift SELF
72 уровень , 4 лекция
Открыта

1. Релиз CLI как контракт

Когда вы впервые собрали LibraryCLI, наверняка было ощущение: «ну всё, программа работает, можно праздновать». И это правда — но только на уровне «я умею печатать Hello, world!». Релиз же начинается там, где появляется другой человек, который запускает ваш инструмент без телепатической связи с автором. И вот тут нужен контракт.

Под «контрактом» я имею в виду не юридический документ на 30 страниц. Речь про набор ожидаемых артефактов и правил: как называется команда, какие аргументы она принимает, где читать справку, что означают ошибки, какой будет код завершения, где посмотреть версию и что поменялось между версиями. Если этот контракт не записан, пользователи (или вы через две недели) начнут «догадываться» — а догадки в программировании обычно заканчиваются багрепортами.

Чтобы лучше держать картинку в голове, удобно представить выпуск как одну небольшую «линию сборки»:

flowchart TD
    A[Код LibraryCLI] --> B[Release build]
    B --> C[Проверка запуска и help]
    C --> D[README.md + примеры команд]
    D --> E[CHANGELOG.md]
    E --> F[git tag v1.0.0]
    F --> G[Готовый релиз-контракт]

Да, технически ваш SwiftPM‑проект может запускаться и без README. Но без README он становится «приложением, которое можно запустить только если вы уже знаете, как его запускать». Звучит как шутка, но это ровно то, что происходит.

2. README.md: чтобы вам не писали в 3 ночи

README обычно воспринимают как «ну это потом, когда будет время». Секрет в том, что «потом» обычно наступает ровно в тот момент, когда вас кто-то спрашивает: «а как запустить?», и вы в ответ присылаете 12 сообщений, одно противоречит другому, и где-то посередине вы понимаете, что проще было бы сразу написать README. Поэтому мы подойдём к README как к части интерфейса: это UI для человека, только текстовый.

SwiftPM‑проекты по своей природе дружат с идеей «минимальный стандартный набор файлов»: пакет имеет Package.swift, исходники лежат в Sources, и CLI запускается через swift run …. README должен отражать эту реальность, а не быть фантазией «как было бы красиво». И особенно важно: в курсе закреплено единое имя запускаемого CLI — LibraryCLI. Оно не должно «прыгать» по документации на library, lib, library-cli, mytool и «вот ещё один псевдоним, чтобы было удобнее».

Практичный README для CLI удобно проектировать таблично: так вы избегаете простыней и случайных пропусков.

Раздел README Что отвечает Зачем это пользователю
Название + короткое описание «Что это?» Понять, туда ли он вообще пришёл
Installation / Build «Как собрать?» Запустить без магии и догадок
Usage «Как пользоваться?» Команды, аргументы, примеры
Data location / Files «Где хранит данные?» Не потерять данные и понимать последствия
Exit codes «Как понять результат в скриптах?» Надёжная автоматизация
Versioning «Какую версию я использую?» Сверка багов и поведения
License (если нужно) «Можно ли это распространять?» Спокойствие для использования

Мини‑шаблон README для LibraryCLI

Ниже пример «скелета». Он специально короткий: ваша задача — не написать роман, а дать работающий справочник.

```md
# LibraryCLI

CLI-инструмент для работы с библиотекой (книги, поиск, обновление данных).

## Build & Run

Сборка и запуск через SwiftPM:

```bash
swift run LibraryCLI help
```

## Usage

```bash
LibraryCLI help
LibraryCLI version
LibraryCLI fetch <id>
```

## Exit codes

Коды завершения соответствуют каноническому `ExitCode` из проекта.
(См. раздел "ExitCode" в исходниках и используйте эти значения в скриптах.)

## Changelog

См. `CHANGELOG.md`.
```

Обратите внимание на важную дисциплину: в примерах мы не пишем «просто swift run», потому что у пакета может быть несколько targets. Мы пишем swift run LibraryCLI, то есть явно указываем исполняемый target. Это соответствует базовому циклу работы со SwiftPM: swift run запускает исполняемую цель пакета, а swift build собирает проект.

3. Примеры команд: копипастно и честно

Пользователь CLI почти всегда делает одно и то же действие: копирует команду из README и запускает. Поэтому примеры команд — это не «для красоты», а буквально ваш «быстрый старт». И тут есть тонкий момент: пример должен быть не только правильным синтаксически, но и самодостаточным. Если пример требует «подготовьте 5 файлов, выставьте 12 переменных окружения, принесите кофе», это не пример, а квест.

SwiftPM по умолчанию создаёт исполняемый пакет так, что точка входа лежит в Sources/main.swift, и запуск возможен через swift run <Name>. Это значит, что в README вы вполне можете честно опираться на команды SwiftPM, не выдумывая «установите через загадочный инсталлятор». Да, в реальном мире бывают установщики, brew‑формулы и прочее, но в рамках курса мы держим контракт простым и воспроизводимым.

Ниже — пример блока «Usage», где команды идут как «мини‑спека» вашего интерфейса. Обратите внимание: мы не вводим альтернативные соглашения именования, везде используется LibraryCLI.

```md
## Usage

Показать справку:

```bash
swift run LibraryCLI help
```

Показать версию:

```bash
swift run LibraryCLI version
```

Загрузить данные по id из сети и сохранить в локальное хранилище:

```bash
swift run LibraryCLI fetch 42
```

```

Если у вас в проекте есть команды add, remove, list, search (что вполне логично для «библиотеки»), вы оформляете их точно так же: «что делает» → «как запустить» → «пример». Тут очень помогает правило: одна команда — один короткий пример. Не нужно писать 12 вариантов, иначе пользователь начинает гадать «какой из них правильный».

Хороший тон — писать примеры так, чтобы они отражали реальную грамматику команд из вашего парсера. Если у команды есть флаг --format json, то пример должен быть с ним. Если флага нет — не добавляйте его «потому что красиво». README не должен опережать код, иначе он превращается в художественную литературу.

4. Релизные артефакты и проверка

Версия и команда version

Версия — это то, что кажется лишним, пока не случился первый баг. Потом версия внезапно становится главным вопросом: «у тебя какой билд?», «а у меня работает», «а у меня нет». Поэтому версия должна быть однозначной, выводиться командой version, и соответствовать git‑тегу. Не «примерно», не «вроде», а буквально совпадать.

SwiftPM позволяет иметь либо main.swift, либо @main‑точку входа, но не оба одновременно. В вашем проекте вы уже выбрали конкретный стиль (скорее всего, @main + static func main()), и команда version — просто ещё одна ветка обработки команд. Сам номер версии при этом лучше держать в одном месте, а не размазывать по строкам в трёх файлах (и в README четвёртым).

Мини‑пример «один источник правды»:

import Foundation

struct BuildInfo {
    static let version = "1.0.0"
}

И где-то в обработчике команды:

import Foundation

func printVersion() {
    print("LibraryCLI \(BuildInfo.version)") // LibraryCLI 1.0.0
}

Да, это всего лишь строка. Но именно эта строка станет тем самым «якорем», который вы будете сравнивать с git tag v1.0.0. И если вы когда-нибудь поймаете себя на мысли «я забыл обновить версию в одном из мест», значит вы случайно сделали два источника правды — и теперь они будут ссориться.

CHANGELOG.md

Changelog часто путают с историей коммитов. Коммиты — это «как мы дошли до состояния», а changelog — «что изменилось для пользователя». Пользователь не обязан понимать вашу внутреннюю кухню: ему важно, появилась ли новая команда, изменился ли формат файла, стали ли ошибки более понятными, поменялись ли exit codes. Поэтому changelog пишется языком поведения, а не языком «переименовал переменную».

В рамках курса вам достаточно простого формата: версии сверху вниз, дата опционально (можно без даты, чтобы не попасть в ловушку «я забыл обновить дату»). Важно другое: v1.0.0 — это «первый стабильный контракт», и в changelog это стоит обозначить явно, даже если вы не меняли мир.

Пример минимального CHANGELOG.md:

# Changelog

## 1.0.0

- Первый стабильный выпуск `LibraryCLI`.
- Зафиксирован контракт команд (см. README).
- Коды завершения соответствуют каноническому `ExitCode`.

Здесь есть список — да, но это тот случай «крайняя необходимость»: changelog без структурированных пунктов превращается в художественный текст, где невозможно быстро понять, что изменилось. Главное — не превращать пункты в простыню.

И ещё один практичный момент: changelog и README должны «дружить». README описывает текущий контракт, changelog описывает его изменения. Если README обещает команду fetch-many, а changelog ни разу не упоминает её появление, это не преступление, но это сигнал, что вы перестали вести историю продукта. А в финальном релизе хочется обратного: ощущение законченности.

Git‑tag v1.0.0

Тег в git — это не «украшение репозитория», а метка «вот именно это состояние кода соответствует версии». И смысл тега в том, что через месяц (или через год) вы можете вернуться к этой версии, собрать её заново и получить то же поведение. Это особенно важно для CLI, который могут использовать скрипты: скрипты не любят сюрпризы.

Мы используем формат vX.Y.Z, и конкретно для финала курса — v1.0.0. Здесь важно не перепутать роли: версия в коде (BuildInfo.version) должна совпасть с тегом, а CHANGELOG.md должен иметь секцию 1.0.0. Если совпадают все три — у вас появляется «треугольник правды»: код ↔ документация ↔ git‑история. Если хоть одна вершина врёт, вы рано или поздно поймаете очень странный баг: «почему команда version пишет 1.0.0, а поведение как в 0.9.3?»

Команды git приведу как схему (мы не углубляемся в git, у вас для этого был отдельный день):

# 1) Убедитесь, что рабочее дерево чистое
git status

# 2) Создайте тег
git tag v1.0.0

# 3) (если используете remote) отправьте тег
git push --tags

Важная дисциплина: тег ставят на коммит, а не на «примерно текущую папку». Поэтому сначала коммитятся README/CHANGELOG/версия, а потом ставится тег. Иначе вы рискуете получить «v1.0.0, в котором забыли добавить README» — выпуск уровня «я программист, но мне некогда».

Exit codes и канонический ExitCode

Exit code — это способ CLI общаться не с человеком, а с другим кодом: bash‑скриптом, CI‑шагом, внешним тулом. Человек видит текст ошибки, а скрипт видит число. Поэтому для релиза важно две вещи: чтобы коды были стабильными и чтобы они были документированы. В курсе вы уже приняли канонический ExitCode, поэтому ваша задача сейчас — не изобрести новый, а аккуратно применить существующий.

Документационно это выглядит просто: в README появляется раздел Exit codes, где вы говорите «коды завершения соответствуют ExitCode из проекта». Вы не маппите ошибки по строкам, не придумываете «ещё один код на всякий случай», и не делаете exit(1) в одном месте и exit(42) в другом «потому что так получилось». Идея финала — чтобы выход был централизован.

Мини‑пример «единая точка завершения» (псевдокод по смыслу, потому что сам ExitCode у вас уже определён в проекте и мы его не объявляем заново):

import Foundation

func finish(_ result: Result<Void, Error>) -> Never {
    switch result {
    case .success:
        exit(ExitCode.ok.rawValue)
    case .failure(let error):
        print(error) // или userMessage, если у вас есть такой контракт
        exit(exitCode(for: error).rawValue)
    }
}

Здесь есть две важные «релизные привычки». Первая: выход действительно делается в одном месте, поэтому политика не размазывается по проекту. Вторая: exitCode(for:) работает по типу или категории ошибки (как вы уже делали раньше), а не по строковому анализу. Это ровно то, что делает CLI предсказуемым: текст сообщения вы можете улучшать сколько угодно, а скрипты останутся рабочими.

Мини‑ритуал перед выпуском

Перед тем как ставить v1.0.0, полезно пройти короткий ритуал проверки. Он не должен быть длинным, иначе его никто не делает. Он не должен быть слишком умным, иначе он превращается в отдельный проект. И он должен проверять именно то, что вы обещаете в README: что CLI запускается, help печатается, version совпадает, основные команды работают хотя бы на простом сценарии.

SwiftPM даёт вам базовые команды для жизненного цикла пакета: swift build, swift run, swift test. В рамках курса релизная дисциплина обычно включает «clean build», чтобы не оказаться в ситуации «у меня работало из-за мусора в .build».

Ниже — компактная схема, которую можно буквально копировать себе в заметки:

swift package clean
swift test
swift build -c release

swift run LibraryCLI help
swift run LibraryCLI version

Заметьте, мы не делаем «сложных» проверок и не требуем профилировщиков. Наша цель — убедиться, что контракт не сломан на самом базовом уровне. Если help не запускается — это не «мелочь», это прямой дефект интерфейса.

5. Типичные ошибки при финальной упаковке

Ошибка №1: README и реальность расходятся.
Самый частый сценарий: README написали «когда-то», потом команды переименовали, грамматику аргументов поменяли, а README остался жить в прошлом. В итоге пользователь копирует пример, получает ошибку, и решает, что инструмент «не работает». Это почти всегда лечится простой дисциплиной: обновлять README в том же PR или коммите, где вы меняете интерфейс команд.

Ошибка №2: имя CLI пляшет по проекту.
В одном месте написано LibraryCLI, в другом librarycli, в третьем «запустите swift run Library». Кажется мелочью, но это мгновенно рушит доверие: если даже имя не стабильное, что уж говорить о форматах данных и ошибках. В курсе имя зафиксировано, так что правило простое: везде LibraryCLI, без синонимов.

Ошибка №3: версия размазана по строкам и неизбежно рассинхронизируется.
Если версия записана в BuildInfo, в README, в changelog, и ещё где-то в тексте help‑сообщения отдельной строкой — вы обязательно забудете обновить одно из мест. Поэтому «один источник правды» в коде плюс синхронизация тега и changelog — это не бюрократия, а защита от глупых ошибок.

Ошибка №4: changelog превращён в «пасту из коммитов».
Когда changelog выглядит как «refactor», «fix», «wip», «merge branch», он перестаёт быть полезным. Для пользователя (и для вас в будущем) важны изменения поведения: новые команды, изменения формата, уточнение сообщений об ошибках, изменения в exit codes. Всё остальное остаётся в истории коммитов, там ему и уютно.

Ошибка №5: exit codes используются как придётся.
Иногда можно встретить проект, где часть кода делает exit(1), часть возвращает «успех» даже при ошибке, а часть падает через fatalError на обычном пользовательском вводе. Для релиза это критично: автоматизация и скрипты начинают жить в мире случайных чисел. Лечится централизованной точкой завершения и использованием канонического ExitCode без «ещё одной таблички» и без маппинга по строкам.

1
Задача
Swift SELF, 72 уровень, 4 лекция
Недоступна
README контракт
README контракт
1
Задача
Swift SELF, 72 уровень, 4 лекция
Недоступна
Changelog релиза
Changelog релиза
1
Задача
Swift SELF, 72 уровень, 4 лекция
Недоступна
Команда version
Команда version
1
Задача
Swift SELF, 72 уровень, 4 лекция
Недоступна
Скрипт релиза
Скрипт релиза
1
Опрос
CLI и производительность, 72 уровень, 4 лекция
Недоступен
CLI и производительность
AsyncStream, прогресс, оптимизация CLI
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ