Руководства · Часть 7 из 8
Привязка релизов из CI
Каждый релиз, который вы выпускаете, — это набор байтов: tarball-архивы, wheel-пакеты, образы контейнеров, диапазон коммитов. Привязка подтверждения существования (Proof of Existence, PoE) Label 309 для этих байтов превращает «поверьте нам на слово, что эта сборка — именно то, что мы опубликовали» в «вот транзакция Cardano, которая это свидетельствует». Вы хешируете каждый артефакт в лист, сворачиваете листья в один корень Меркла и публикуете этот единственный корень в блокчейне под меткой метаданных 309. С этого момента любой, у кого есть ссылка на транзакцию, может доказать, что артефакт существовал во время его блока или раньше, — опираясь лишь на публичный блокчейн, без аккаунта и без доверия к вашему конвейеру или вашему поставщику.
Байты артефакта никогда не покидают раннер. CLI хеширует локально и публикует только дайджесты, поэтому это безопасно для приватных репозиториев и сборок с закрытым исходным кодом: в блокчейн попадает хеш фиксированной длины, а не ваш код.
Главный инструмент: cardanowall attest
Всё на этой странице выполняется одной командой независимого от шлюза CLI cardanowall (крейт cardanowall-cli на crates.io, с готовыми бинарными файлами в релизах label-309-cli). attest — это точка входа, заточенная под CI: она хеширует ваши входные данные, запрашивает расценку и публикует запись через шлюз, а затем ждёт состояния жизненного цикла, которое вы укажете.
Направьте её на любой шлюз Label 309, задав базовый URL и API-ключ с областью публикации, а затем передайте ей то, что нужно захешировать:
export CARDANOWALL_API_KEY="…" # a publish-scoped key from your gateway
cardanowall attest \
--paths 'dist/*' \
--base-url https://your-gateway.example/api/v1 \
--wait confirmed \
--receipt-out poe-receipt.json--base-url и --api-key также читаются из CARDANOWALL_BASE_URL и CARDANOWALL_API_KEY, поэтому в CI, где они заданы из вашего хранилища секретов, оба выпадают из команды.
Три способа выбрать, что привязывать
Задайте ровно один вход; режим следует из него.
Файлы. --paths принимает буквальный путь или glob-шаблон, повторяемо. Каждый лист — это SHA-256 байтов файла. Выборка дедуплицируется и сортируется побайтно по нормализованному относительному пути, поэтому одно и то же рабочее дерево всегда даёт один и тот же корень независимо от порядка, в котором glob раскрыла бы оболочка. Заключите glob в кавычки, чтобы оболочка не раскрыла его заранее:
cardanowall attest --paths 'dist/**/*.tar.gz' --paths 'dist/**/*.whl'Коммиты. --commits принимает диапазон git rev-list; каждый лист — это SHA-256 сырого объекта коммита, от старого к новому. Так привязывается происхождение самой истории. Требуется полная история git на раннере, поскольку поверхностный клон не может разрешить диапазон:
cardanowall attest --commits v1.0.0..v1.1.0Заранее вычисленные дайджесты. --leaf принимает 64-символьный шестнадцатеричный дайджест, вычисленный вами где-то ещё, повторяемо и с сохранением порядка аргументов. Используйте его, чтобы привязать то, что CLI никогда не видит как файл, например дайджест OCI-образа:
cardanowall attest --leaf 9f86d0818840…0a08 # a 64-hex digest, e.g. an image digestОдин лист публикует запись с единственным элементом; несколько листьев публикуют запись Меркла, корень которой привязывается к блокчейну, а список листьев загружается, чтобы для каждого элемента позже можно было получить сертификат включения.
Манифест и квитанция
В файловом режиме attest пишет детерминированный poe-manifest.json рядом со своим выводом (переименуйте его через --manifest-out). Манифест фиксирует привязку «имя — хеш» для каждого привязанного файла, и одни и те же входные данные всегда дают побайтно идентичные байты манифеста. Добавьте --anchor-manifest, чтобы вложить SHA-256 манифеста последним листом, — тогда связь между именами файлов и хешами сама становится частью того, к чему привязывается корень.
--receipt-out пишет версионированную JSON-квитанцию, несущую запись, расценку, транзакцию и снимок ожидания. Сохраните её как доказательство вашей сборки: это всё, что нужно последующей команде verify, чтобы найти и проверить привязку. Сохраните её как артефакт рабочего процесса, приложите к релизу или закоммитьте рядом с changelog.
Ожидание, состояние «в ожидании» и повторные запуски
По умолчанию attest ждёт, пока транзакция пересечёт порог подтверждений (--wait confirmed); --wait submitted возвращается, как только она достигает сети. У ожидания есть срок (--timeout, по умолчанию 600 секунд). Если срок истекает, вывод и квитанция всё равно записываются, а процесс завершается с кодом 3 (в ожидании): публикация не теряется, она продолжается на шлюзе, и вы можете перепроверить её позже по квитанции. Потолок --max-usd отклоняет публикацию (код выхода 1, до какой-либо загрузки), когда расценка превышает ваш лимит, так что скачок цены никогда не выставит конвейеру счёт неожиданно.
Повторные запуски безопасны по построению. attest по умолчанию не отправляет заголовок идемпотентности; вместо этого шлюз дедуплицирует побайтно идентичные записи, поэтому повторный запуск той же сборки никогда не привязывает во второй раз и не тарифицирует дважды. Привяжите тот же dist/ снова — и второй запуск бесплатно повторит первую запись.
GitHub Actions
Действие cardanowall/poe-attest оборачивает тот же CLI для рабочих процессов GitHub. Оно с открытым исходным кодом и закреплено на уровне цепочки поставок: оно встраивает SHA-256-дайджесты запускаемого им релиза CLI и перед каждым запуском проверяет и скачанный архив, и извлечённый бинарный файл, так что подменённый ассет релиза не пройдёт.
Сохраните в репозитории два секрета — GATEWAY_URL (базовый URL плоскости данных вашего шлюза, оканчивающийся на /api/v1) и GATEWAY_API_KEY (ключ с областью публикации), — а затем привязывайте ассеты релиза при его публикации:
name: anchor-release
on:
release:
types: [published]
permissions:
contents: read
jobs:
attest:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: cardanowall/poe-attest@v1
with:
gateway-url: ${{ secrets.GATEWAY_URL }}
api-key: ${{ secrets.GATEWAY_API_KEY }}
paths: |
dist/**/*.tar.gz
dist/**/*.whlШаг пишет квитанцию, печатает сводку с транзакцией и ссылкой на проверку и предоставляет выходные значения (tx, record-id, verify-url и другие) для последующих шагов.
Подпись потока выделенной CI-идентичностью
Чтобы привязки конвейера были атрибутируемыми и находимыми, подписывайте каждую запись сидом идентичности. Тогда шлюз индексирует запись по открытому ключу подписанта, так что любой может перечислить всю историю конвейера из ленты записей шлюза с помощью ?signer=<public-key>. Подпись остаётся необязательной, и верификаторы никогда её не требуют.
Используйте выделенную одноразовую идентичность для каждого конвейера, никогда не личный сид. Сохраните его как секрет, защищённый окружением, чтобы читать его могли только запуски из этого окружения:
jobs:
attest:
runs-on: ubuntu-latest
environment: release-signing
steps:
- uses: actions/checkout@v7
- uses: cardanowall/poe-attest@v1
with:
gateway-url: ${{ secrets.GATEWAY_URL }}
api-key: ${{ secrets.GATEWAY_API_KEY }}
seed: ${{ secrets.CI_SIGNING_SEED }}
paths: dist/**/*.tar.gzСид маскируется в логах и передаётся CLI только через stdin, никогда в командной строке, и ни один секрет никогда не достигает шлюза: хеширование и подпись происходят локально, и публикуются только запись и публичные данные.
Никогда не привязывайте из pull_request_target
Этот триггер открывает секреты вашего репозитория коду из форкнутых pull request'ов. Привязывайте
только из событий, которые вы контролируете, например release, push или workflow_dispatch.
Минимальному заданию выше нужен лишь contents: read; добавляйте contents: write только если вы
также прикрепляете квитанцию к релизу.
Действие несёт больше входных параметров, чем показано здесь, в том числе сертификат включения на каждый лист, прикрепление ассетов к релизу, потолок цены и политику тайм-аута. Полный набор см. в README действия.
GitLab CI/CD
В GitLab та же обёртка поставляется как компонент CI/CD. Задание выполняется внутри собственного образа контейнера CLI, закреплённого по версии и дайджесту, так что во время запуска ничего не устанавливается — и, как и всюду на этой странице, подойдёт любой шлюз Label 309, размещённый оператором или свой собственный:
include:
- component: gitlab.com/cardanowall/poe-attest/attest@1
inputs:
gateway-url: https://your-gateway.example/api/v1
paths: |
dist/**/*.tar.gz
dist/**/*.whlДобавьте секреты как переменные CI/CD в Settings → CI/CD → Variables, с флагами masked и protected: CARDANOWALL_API_KEY (ключ с областью публикации) и, только если вы подписываете, CARDANOWALL_SEED — задавайте его на самом проекте, никогда на группе: наследование молча подписывало бы привязки каждого дочернего проекта одной идентичностью. По умолчанию задание выполняется только в пайплайнах защищённых тегов, так что незащищённая ветка никогда не потратит ваш баланс; переопределите входной параметр rules, чтобы привязывать по другим событиям.
Результаты возвращаются dotenv-отчётом: последующее задание, указавшее задание привязки в needs:, напрямую читает транзакцию, ссылку для проверки и остальные из восемнадцати переменных POE_*:
announce:
needs: [poe-attest]
script:
- echo "anchored in $POE_TX"
- echo "verify at $POE_VERIFY_URL"Компонент несёт больше входных параметров, чем показано здесь, в том числе сертификаты включения, потолок цены, теги раннеров и политику тайм-аута. Полный набор см. в README компонента.
Другие системы CI
Тот же CLI работает где угодно. Используйте образ контейнера ghcr.io/cardanowall/label-309-cli (его entrypoint — cardanowall) или готовый бинарный файл со страницы релизов.
Любой другой раннер, с бинарным файлом в PATH:
export CARDANOWALL_BASE_URL="https://your-gateway.example/api/v1"
export CARDANOWALL_API_KEY="$YOUR_CI_SECRET"
cardanowall attest \
--paths 'dist/*' \
--wait confirmed \
--receipt-out poe-receipt.json
# exit 0 = reached the wait target; 3 = pending (publish continues on the gateway);
# 1 = refused (for example over --max-usd) or failed.Что вам нужно от шлюза
Публикация помещает транзакцию в Cardano, а это стоит комиссии, поэтому attest нужен шлюз, через который её отправить. Подойдёт любой шлюз Label 309: размещённый оператор или ваш собственный самостоятельно размещённый шлюз (открытый label-309-gateway, один бинарный файл Rust плюс Postgres). Из CI вам нужны от него лишь две вещи: базовый URL плоскости данных и API-ключ с областью публикации (poe:create), обеспеченный предоплаченным балансом.
Шлюз владеет финансируемым кошельком Cardano и платит комиссию из своей собственной модели баланса. Ваша CI не хранит ни ключа кошелька, ни средств в блокчейне. Худшее, что может сделать утёкший API-ключ, — потратить предоплаченный баланс этого аккаунта на новые привязки; он не может перемещать средства, читать ваш контент или подписывать за вас. Ротируйте или отзывайте его в любой момент.
Проверка привязки
Эта привязка чего-то стоит именно потому, что любой может проверить её без вас. Имея ссылку на транзакцию из квитанции, проверка выполняется автономно по публичному блокчейну и обозревателю на ваш выбор, без аккаунта и без шлюза:
cardanowall verify <tx-hash>Она разрешает транзакцию, структурно валидирует запись, проверяет любую подпись, подтверждает, что запись зафиксирована, и возвращает вердикт своим кодом выхода, поэтому она встраивается в последующую проверку так же чисто, как attest встраивается на стороне публикации. Чтобы подтвердить отдельный артефакт по его привязке, захешируйте файл и сравните, либо для записи Меркла постройте сертификат включения, который прикрепляет артефакт к опубликованному корню. Полная модель верификатора — в разделе Проверка.
Доказательство переживает конвейер
Привязка Label 309 — это обычные метаданные под меткой 309, а не квитанция поставщика. Спустя долгое время после того, как раннер исчез, реестр был ротирован, а система CI осталась лишь воспоминанием, транзакция по-прежнему свидетельствует, что ваши артефакты существовали во время их блока. Любой может проверить её по публичному блокчейну, без аккаунта и без доверия к тому, кто её опубликовал.