DevOps із Claude Code: середовище, CI/CD і build automation

Локальний verification harness уже задає контракт якості. Тепер перенесемо його на чистий runner, збережемо один перевірений artifact і дамо Claude вузьку роль: пояснювати failure, але не ухвалювати рішення за pipeline. Наскрізний приклад - внутрішній репозиторій PulseDesk, де PR-код і AI secret ніколи не опиняються в одному job.

Сьогодні пройдемо: рівень 21. Git, pull request, project scripts і локальний verification harness вважаються робочим тлом.

Локально зелено. 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 поки не відтворюваний.

Запит "Claude, полагодь pipeline" занадто широкий. Спочатку треба довести, який input відрізняється і де має жити його контракт.

CI переносить contract на чистий runner

CI отримує подію репозиторію, піднімає тимчасове середовище і виконує версіоновані команди проєкту. Його продуктом стають checks, logs і artifacts, за якими людина ухвалює рішення.

flowchart LR A["Local commit"] --> B["CI event"] B --> C["Clean runner"] C --> D["Checks and artifact"] D --> E["Human decision"]

Діаграма відокремлює подію від виконавця: pull_request запускає workflow, GitHub runner виконує команди, а Claude тут не trigger і не gate.

Цінність CI не в хмарі, а в повторенні одного контракту в чистому середовищі.

Event, job, artifact і human decision так само розкладаються в GitLab CI, Bitbucket Pipelines та Jenkins. Синтаксис змінюється, trust boundary залишається.

PR-код і secret живуть у різних jobs

Навіть у внутрішньому репозиторії довірений автор не робить змінюваний PR-код безпечним для job із секретом. Межа будується архітектурою, а не обіцянкою prompt.

JobОтримуєНе отримує
verifycheckout PR, runtime, project checksAnthropic key, production credentials
diagnosebounded redacted artifact, Anthropic keycheckout, project config, write access, tools

AI-diagnosis доступний лише для same-repo PR від людини. Verify виконується для будь-якого PR, просто без secrets: fork PR і Dependabot отримують deterministic verification, а raw evidence зберігається для reviewer.

Read-only означає відсутність небезпечних capabilities і контексту, а не фразу "нічого не змінюй".


Claude пояснює gate, але не стає gate

Pipeline ділиться на дві лінії. Deterministic checks продукують exit code і обов'язковий статус. Advisory job інтерпретує вже зібрані дані і повертає підказку людині.

flowchart LR PR["PR code"] subgraph gate["Deterministic lane"] V["Checks"] --> S["Check status"] S --> R["Required check policy"] end subgraph advisory["Advisory lane"] E["Raw evidence"] --> A["Claude diagnosis"] end PR --> V V -->|failure| E R --> H["Human merge decision"] A --> H

Верхня лінія задає check status, який ruleset може зробити обов'язковим. Нижня вмикається після failure: Claude допомагає людині прочитати evidence, але його висновок не змінює status.

Merge блокує не exit code сам собою, а required check у ruleset або branch protection. Фраза Claude "зміна безпечна" не вважається CI signal.

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

Та сама картка, якщо розгорнути її в питання:

Workflow стає зрозумілим, коли YAML лише кодує вже ухвалений delivery contract.

Для невеликого repo почніть із deterministic verify. AI diagnosis додавайте, коли розбір червоних runs справді забирає час команди.

Червоний 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 manifestsetup читає той самий version file
dependencieslockfilenpm ci, без оновлення lock
configschema + .env.examplestartup validation і config check
commandspackage.json, scripts/ciCI викликає project scripts
runtime behaviorзібраний artifactstartup + smoke того самого файлу

Для PulseDesk спокуса проста - дописати REPORT_TIMEZONE: UTC у workflow і закрити тікет. Замало. Системний fix включає startup validation, безпечний приклад значення і deterministic check повноти config contract.

Cache дозволено втратити без зміни результату. Якщо clean install і cached install дають різні verdict, cache став прихованим input.

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.

Без checkout у Claude немає PR-файлів. --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.

ubuntu-latest залишається moving base. Тут закріплені керовані inputs проєкту, але це не bit-for-bit образ машини.

Build один раз. Перевіряємо той самий artifact

Якщо build після тестів запускається ще раз, другий результат уже не покритий попереднім evidence. Тому pipeline зберігає identity одного artifact від збірки до review.

flowchart LR A["Commit SHA"] --> B["Build once"] B --> C["Artifact plus checksum"] C --> D["Smoke same artifact"] D --> E["Store for review"]

Стрілки означають походження, а не повторну збірку: checksum пов'язує збережений файл із конкретним build result і commit.

Manifest пише фактичний git rev-parse HEAD на runner. Для pull_request це може бути synthetic merge commit GitHub: доведено artifact саме цього checkout.

Rebuild між smoke і upload створює artifact B. Зелений smoke artifact A більше нічого не доводить про збережений файл.

Logs і artifacts переживають runner

Runner зникне після job. Отже, workflow заздалегідь визначає мінімальний storage contract для успіху і failure.

СценарійЗберігаємоНавіщо
successartifact, checksum, manifestreview того самого build result
failureredacted bounded log, metadataдіагностика без workspace
AI resultdiagnosis.json + provenanceпорада поруч із raw evidence

Дрібниці, які тут вирішують:

Не зберігаємо 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

sequenceDiagram participant Dev as "Developer" participant Ver as "Verify job" participant Store as "Artifact storage" participant Dia as "Diagnose job" participant Repo as "Config contract" Dev->>Ver: PR verification Ver-->>Dev: smoke failed REPORT_TIMEZONE Ver->>Store: upload verify-failure Store->>Dia: download verify-failure Dia-->>Dev: environment plus next_check Dev->>Repo: compare schema and env example Dev->>Repo: add validation and safe example Dev->>Ver: targeted rerun Ver-->>Dev: artifact checksum and manifest

Jobs не ділять workspace: їх пов'язує named artifact. Claude повертає гіпотезу, developer підтверджує її за schema і .env.example, потім запускає targeted rerun.

Перевірена diagnosis змінює contract, а не маскує symptom. Fix додає startup validation, безпечний приклад значення, CI env і deterministic config check.

Після зміни config contract запускаємо один affected job. Повтор без нового evidence нічого не діагностує.

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.

Proposal не evidence. Verdict дає лише реальний run з exit code, log і artifact identity.

Фінал: delivery evidence замість works on my machine

flowchart LR PR["PR code"] --> V["Verify without secrets"] V --> A["Artifact and raw evidence"] A --> D["Diagnose without checkout"] D --> H["Human decision"] S["Anthropic secret"] --> D

Схема тримає головну межу: змінюваний PR-код виконується без secrets, а secret-bearing diagnosis бачить лише bounded evidence і віддає пораду людині.

  1. Опишіть event, runner і trust boundary.
  2. Виконайте project-owned verify без secrets.
  3. Закріпіть runtime, dependencies і config contract.
  4. Зберіть, перевірте і збережіть один artifact.
  5. Передайте Claude bounded failure без checkout.
  6. Перевірте пораду людиною і збережіть evidence rerun.
Release support тут обмежений draft notes із прийнятого manifest. Version bump, publish і deploy залишаються окремими операціями з окремими правами.

Зрілий pipeline не просить AI керувати delivery. Він будує перевірюваний evidence, а AI допомагає людині швидше його прочитати.