DevOps із Claude Code: середовище, CI/CD і build automation
Локальний verification harness уже задає контракт якості. Тепер перенесемо його на чистий runner, збережемо один перевірений artifact і дамо Claude вузьку роль: пояснювати failure, але не ухвалювати рішення за pipeline. Наскрізний приклад - внутрішній репозиторій PulseDesk, де PR-код і AI secret ніколи не опиняються в одному job.
- спроєктуємо CI contract до написання YAML;
- відтворимо середовище через runtime, lockfile і config schema;
- розділимо deterministic gate і advisory AI-diagnosis;
- перевіримо PR-код без secrets, а failure розберемо без checkout;
- зберемо, перевіримо і збережемо один і той самий artifact;
- дамо Claude запропонувати build contract, а runner змусимо його довести.
Локально зелено. Delivery ще не доведено
PulseDesk формує SLA-звіт для команди підтримки. На машині розробника весь harness зелений, але чистий CI runner зупиняє застосунок до smoke-check.
LOCAL
$ npm run verify
lint PASS | typecheck PASS | tests PASS | build PASS
CI RUNNER
$ npm run smoke
ConfigError: REPORT_TIMEZONE is required
Локальний .env містить REPORT_TIMEZONE=UTC, а .env.example і workflow цього не обіцяють. Код однаковий, входи середовища різні.
Чистий runner перевіряє не ноутбук, а повноту репозиторного контракту. Якщо обов'язковий input існує лише в особистому середовищі, delivery поки не відтворюваний.
CI переносить contract на чистий runner
CI отримує подію репозиторію, піднімає тимчасове середовище і виконує версіоновані команди проєкту. Його продуктом стають checks, logs і artifacts, за якими людина ухвалює рішення.
Діаграма відокремлює подію від виконавця: pull_request запускає workflow, GitHub runner виконує команди, а Claude тут не trigger і не gate.
- runner тимчасовий і не знає особистий
.env; - runtime, dependencies і команди мають читатися з репозиторію;
- між runs зберігаються лише явно завантажені logs і artifacts.
Цінність CI не в хмарі, а в повторенні одного контракту в чистому середовищі.
PR-код і secret живуть у різних jobs
Навіть у внутрішньому репозиторії довірений автор не робить змінюваний PR-код безпечним для job із секретом. Межа будується архітектурою, а не обіцянкою prompt.
| Job | Отримує | Не отримує |
|---|---|---|
verify | checkout PR, runtime, project checks | Anthropic key, production credentials |
diagnose | bounded redacted artifact, Anthropic key | checkout, project config, write access, tools |
AI-diagnosis доступний лише для same-repo PR від людини. Verify виконується для будь-якого PR, просто без secrets: fork PR і Dependabot отримують deterministic verification, а raw evidence зберігається для reviewer.
verifyвиконує код, але не бачить secrets;diagnoseбачить secret, але не виконує і не читає PR-код;- привілейований trigger для fork PR не додається.
Read-only означає відсутність небезпечних capabilities і контексту, а не фразу "нічого не змінюй".
Claude пояснює gate, але не стає gate
Pipeline ділиться на дві лінії. Deterministic checks продукують exit code і обов'язковий статус. Advisory job інтерпретує вже зібрані дані і повертає підказку людині.
Верхня лінія задає check status, який ruleset може зробити обов'язковим. Нижня вмикається після failure: Claude допомагає людині прочитати evidence, але його висновок не змінює status.
Workflow проєктують до YAML
YAML добре виконує рішення і погано їх заміняє. До синтаксису потрібні відповіді на шість питань: подія, runner, команди, artifacts, permissions і failure path.
scope: внутрішній repo; same-repo human PR
event: pull_request; fork і Dependabot без AI diagnosis
runner: Node з .nvmrc + npm ci
verify: lint -> typecheck -> tests -> build -> smoke
success: pulsedesk-reporting.tgz + checksum
failure: bounded redacted verify.log + metadata
AI: без checkout і tools; untrusted diagnosis.json
decision: developer або reviewer
Та сама картка, якщо розгорнути її в питання:
- який event запускає роботу і який commit перевіряється;
- які project-native команди утворюють gate;
- що переживає runner і скільки зберігається;
- які права потрібні кожному job;
- що відбувається після червоного кроку;
- хто ухвалює фінальне рішення.
Workflow стає зрозумілим, коли YAML лише кодує вже ухвалений delivery contract.
Червоний verify не запускає diagnose сам
Після failed step звичайні кроки пропускаються, а downstream job залежить від результату попереднього job. Failure path треба записати явно.
jobs:
verify:
runs-on: ubuntu-latest
steps:
- name: Verify and keep status
shell: bash
run: |
mkdir -p artifacts
./scripts/ci/verify.sh 2>&1 | tee artifacts/verify.log
- name: Upload failure input
if: ${{ failure() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: verify-failure
path: artifacts/verify.log
if-no-files-found: error
retention-days: 7
diagnose:
needs: verify
if: >-
${{ always() && needs.verify.result == 'failure' &&
github.event.pull_request.head.repo.full_name == github.repository &&
github.actor != 'dependabot[bot]' }}
runs-on: ubuntu-latest
steps:
- name: Download failure input
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
with:
name: verify-failure
path: artifacts
always() дозволяє оцінити failed dependency. Умова допускає diagnosis лише для same-repo PR і виключає Dependabot, якому Actions secrets усе одно недоступні. Може впасти і сам diagnose - мережа, ліміти API. Merge це не блокує: job не required, а raw evidence уже збережено.
shell: bash запускає зовнішній step із -o pipefail, тому failure у verify.sh | tee не губиться. Усередині самого verify.sh усе ще потрібен set -euo pipefail для його власних pipelines.Clean runner розкриває приховане середовище
Відтворюваність починається там, де кожен змінюваний input має версіоноване джерело. Runner лише показує місця, які локальна машина раніше маскувала.
| Input | Джерело правди | Як ловимо drift |
|---|---|---|
| runtime | .nvmrc або tool manifest | setup читає той самий version file |
| dependencies | lockfile | npm ci, без оновлення lock |
| config | schema + .env.example | startup validation і config check |
| commands | package.json, scripts/ci | CI викликає project scripts |
| runtime behavior | зібраний artifact | startup + smoke того самого файлу |
Для PulseDesk спокуса проста - дописати REPORT_TIMEZONE: UTC у workflow і закрити тікет. Замало. Системний fix включає startup validation, безпечний приклад значення і deterministic check повноти config contract.
Secret-bearing job працює без checkout
diagnose продовжує artifact handoff, але репозиторій не завантажує. Job отримує лише bounded log, pinned toolchain і secret на одному execution step.
permissions: {}
steps:
- uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
with:
name: verify-failure
path: artifacts
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "22"
package-manager-cache: false
- name: Install pinned Claude CLI
run: npm install -g @anthropic-ai/claude-code@2.1.220
- name: Diagnose without checkout
shell: bash
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
# повна JSON schema лежить у repository variable, розбір далі
DIAGNOSIS_SCHEMA: ${{ vars.DIAGNOSIS_SCHEMA }}
run: |
test -s artifacts/verify.log
test "$(wc -c < artifacts/verify.log)" -le 200000
claude --bare -p "Класифікуй CI failure." --tools "" \
--max-turns 3 --no-session-persistence \
--output-format json --json-schema "$DIAGNOSIS_SCHEMA" \
< artifacts/verify.log \
| jq -e '.structured_output' > artifacts/diagnosis.json
Невиконуваний output залишається даними. Поле next_check можна обговорити, але не можна підставляти в run, eval або автоматичний rerun.
--bare прибирає project customizations, а --tools "" вимикає вбудовані tools.Deterministic job залишається нудним
Хороший verify job майже не містить логіки. Він задає межі виконання і викликає один project-owned script.
name: PR verification
on:
pull_request:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
verify:
runs-on: ubuntu-latest
timeout-minutes: 15
env:
REPORT_TIMEZONE: UTC
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Node from repository
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: .nvmrc
cache: npm
- name: Verify and keep status
shell: bash
run: |
mkdir -p artifacts
./scripts/ci/verify.sh 2>&1 | tee artifacts/verify.log
verify.sh включає set -euo pipefail, запускає дешеві перевірки раніше за дорогі, збирає artifact один раз і перевіряє його до upload. Крок upload failure-артефакту тут опущено - він показаний вище і живе в цьому ж job.
timeout-minutesобмежує завислий run;concurrencyскасовує застарілу перевірку того самого PR;- повні action SHA зменшують ризик неочікуваної зміни dependency.
ubuntu-latest залишається moving base. Тут закріплені керовані inputs проєкту, але це не bit-for-bit образ машини.Build один раз. Перевіряємо той самий artifact
Якщо build після тестів запускається ще раз, другий результат уже не покритий попереднім evidence. Тому pipeline зберігає identity одного artifact від збірки до review.
Стрілки означають походження, а не повторну збірку: checksum пов'язує збережений файл із конкретним build result і commit.
- build створює
pulsedesk-reporting.tgz; - smoke розпаковує і запускає саме цей архів;
- manifest зберігає commit SHA, runtime і dependency lock hash;
- upload приймає artifact і checksum після успішного smoke.
Manifest пише фактичний git rev-parse HEAD на runner. Для pull_request це може бути synthetic merge commit GitHub: доведено artifact саме цього checkout.
Logs і artifacts переживають runner
Runner зникне після job. Отже, workflow заздалегідь визначає мінімальний storage contract для успіху і failure.
| Сценарій | Зберігаємо | Навіщо |
|---|---|---|
| success | artifact, checksum, manifest | review того самого build result |
| failure | redacted bounded log, metadata | діагностика без workspace |
| AI result | diagnosis.json + provenance | порада поруч із raw evidence |
Дрібниці, які тут вирішують:
- redaction живе всередині
verify.sh- лог пишеться вже через фільтр, і в upload, і до Claude йде очищена версія; if-no-files-found: errorловить зламаний failure path;- provenance: commit SHA + input digest + model/CLI version.
Не зберігаємо workspace, .env, credentials, нескінченний debug-log або transcript замість вихідного failure.
Найкращий diagnostic artifact не найбільший, а мінімально достатній для наступного рішення.
Read-only Claude розбирає червоний job
diagnose отримує один перевірений log, вимикає tools і вимагає JSON за schema. Код репозиторію в job відсутній, а shell відхиляє порожній або невалідний результат.
set -euo pipefail
schema='{
"type": "object",
"properties": {
"failure_type": {"enum": ["code", "dependency", "environment", "flaky", "infrastructure", "unknown"]},
"evidence": {"type": "string"},
"next_check": {"type": "string"}
},
"required": ["failure_type", "evidence", "next_check"],
"additionalProperties": false
}'
claude --bare -p \
"Класифікуй CI failure. Поверни failure_type, evidence і один next_check. Код не змінюй." \
--tools "" \
--max-turns 3 \
--no-session-persistence \
--output-format json \
--json-schema "$schema" \
< artifacts/verify.log \
| jq -e '.structured_output' \
> artifacts/diagnosis.json
{
"failure_type": "environment",
"evidence": "REPORT_TIMEZONE is required during smoke",
"next_check": "compare config schema and .env.example"
}
Цей JSON і є значення repository variable DIAGNOSIS_SCHEMA із workflow - env читає його через vars, а не інлайнить у YAML. Schema перевіряє форму відповіді, але не істинність змісту. Reviewer звіряє evidence з raw log і вирішує, чи виконувати запропоновану перевірку.
diagnosis.json залишається untrusted data. Його поля не потрапляють у shell, permissions, workflow expressions або автоматичні дії.PulseDesk: від червоного job до прийнятого artifact
Jobs не ділять workspace: їх пов'язує named artifact. Claude повертає гіпотезу, developer підтверджує її за schema і .env.example, потім запускає targeted rerun.
Перевірена diagnosis змінює contract, а не маскує symptom. Fix додає startup validation, безпечний приклад значення, CI env і deterministic config check.
Claude пропонує contract. Runner його доводить
Коли build contract ще не записано, Claude корисний до YAML: читає факти репозиторію і збирає proposal. Жодних здогадок замість відсутніх даних.
Прочитай .nvmrc, package-lock.json, package.json і config schema.
Поверни:
1. runtime і clean install command;
2. project-native verify command;
3. artifact path і smoke command;
4. unknowns.
Файли не змінюй. Якщо даних немає, пиши unknown.
runtime: Node 22 з .nvmrc
install: npm ci за package-lock.json
verify: npm run verify із package.json
artifact: dist/pulsedesk-reporting.tgz із build script
smoke: npm run smoke -- dist/pulsedesk-reporting.tgz
unknown: REPORT_TIMEZONE немає в .env.example
Команда отримує draft, порівнює його з репозиторієм і кодує підтверджене в project scripts. Потім runner виконує ці scripts на clean install.
Фінал: delivery evidence замість works on my machine
Схема тримає головну межу: змінюваний PR-код виконується без secrets, а secret-bearing diagnosis бачить лише bounded evidence і віддає пораду людині.
- Опишіть event, runner і trust boundary.
- Виконайте project-owned verify без secrets.
- Закріпіть runtime, dependencies і config contract.
- Зберіть, перевірте і збережіть один artifact.
- Передайте Claude bounded failure без checkout.
- Перевірте пораду людиною і збережіть evidence rerun.
Зрілий pipeline не просить AI керувати delivery. Він будує перевірюваний evidence, а AI допомагає людині швидше його прочитати.