CI на GitHub Actions — это YAML-файл в .github/workflows/, который на каждый pull request ставит зависимости через npm ci, запускает линтер, проверку типов, тесты и сборку на чистой машине GitHub. Чтобы сломанный код не попал в main, итоговую проверку делают обязательной в защите ветки, а публикацию и деплой выносят в отдельный workflow по тегу — с минимальными правами токена, actions, закреплёнными по SHA, и секретами окружения production.
После материала вы сможете
- написать
ci.ymlдля Node.js-проекта:npm ciс кэшем, линтер, проверка типов, unit-тесты в матрице версий Node.js, интеграционные тесты с PostgreSQL в сервисном контейнере и сборка Docker-образа - ограничить права
GITHUB_TOKEN, закрепить actions по SHA и передавать недоверенный ввод вrunтолько через переменные окружения - сделать одну итоговую проверку обязательной для слияния и объяснить, почему пропущенное задание не блокирует pull request
- проверить workflow до push через
actionlintиactи настроить публикацию образа и деплой по тегу черезenvironment
Проверено на
GitHub Actions (runner ubuntu-24.04)actions/checkout 7.0.1actions/setup-node 7.1.0actions/upload-artifact 7.0.2docker/build-push-action 7.4.0Node.js 22 / 24 / 26actionlint 1.7.12act 0.2.89
Сверено с документацией: 11 октября 2026 г.
Содержание
- CI и CD: что проверять автоматически
- Как устроен workflow: события, jobs, steps
- Проверки на pull request: Node.js, кэш, матрица, PostgreSQL
- Безопасность workflow: права, версии actions, недоверенный ввод
- Проверка до push: actionlint и локальный прогон через act
- Обязательные проверки: защита ветки
- Публикация образа и деплой по тегу
- Упражнения
CI и CD: что проверять автоматически
CI (continuous integration, непрерывная интеграция) — практика, при которой каждое изменение автоматически собирается и проверяется до слияния в основную ветку. CD (continuous delivery/deployment) — следующий шаг: проверенная версия так же автоматически публикуется и выкатывается на сервер. В GitHub Actions и то и другое описывается YAML-файлами в каталоге .github/workflows/ самого репозитория — конвейер живёт рядом с кодом и меняется через тот же pull request.
Практика урока — API заявок из материала о тестировании backend: Node.js, Express, PostgreSQL, unit- и интеграционные тесты на Vitest. Мы поставим его на конвейер: каждый pull request проходит линтер, проверку типов, unit-тесты на трёх версиях Node.js, интеграционные тесты с настоящим PostgreSQL и сборку Docker-образа, а тег вида v1.2.0 публикует образ и разворачивает его в production.
pull_request
- Что запускается
ci.yml: все проверки, образ собирается без публикации- Зачем
- не пустить в
mainкод, который не проходит проверки
push в main
- Что запускается
ci.yml- Зачем
- проверить результат слияния и обновить кэш ветки
main, которым пользуются все pull request
тег v*.*.*
- Что запускается
release.yml: CI → публикация образа → деплой- Зачем
- выпустить версию тем же проверенным способом, а не руками с ноутбука
Как устроен workflow: события, jobs, steps
Workflow — один YAML-файл. Ключ on перечисляет события, на которые он реагирует, jobs — задания. Каждое задание GitHub запускает на отдельной чистой виртуальной машине — раннере (runner), которую выбирает runs-on. Задания идут параллельно, если их не связать через needs. Внутри задания по порядку выполняются шаги (steps): либо команда оболочки run, либо готовое действие (action) через uses — например, actions/checkout скачивает код репозитория.
Событие (on)
- Что это
- что запускает workflow
- В нашем
ci.yml pull_request,pushвmain,workflow_call— вызов из другого workflow
Задание (job)
- Что это
- набор шагов на одной машине; результат — отдельная проверка в pull request
- В нашем
ci.yml checks,unit,integration,image,ci-ok
Раннер
- Что это
- машина, где выполняется задание
- В нашем
ci.yml ubuntu-24.04— образ Ubuntu от GitHub с Node.js и Docker
Action
- Что это
- готовый переиспользуемый шаг из другого репозитория
- В нашем
ci.yml actions/setup-nodeставит Node.js и кэширует npm
Матрица
- Что это
- одно задание, размноженное по набору значений
- В нашем
ci.yml node: [22, 24, 26]— три задания unit-тестов
Сервисный контейнер
- Что это
- Docker-контейнер, который живёт рядом с заданием
- В нашем
ci.yml postgres:17-alpineдля интеграционных тестов
Две особенности события pull_request, о которых стоит знать заранее. Во-первых, проверяется не последняя версия ветки, а результат её слияния с целевой веткой: GITHUB_REF равен refs/pull/<номер>/merge, и actions/checkout скачивает именно этот merge-коммит. Во-вторых, если в pull request конфликт слияния, workflow на pull_request не запускается вообще — сначала нужно разрешить конфликт. По умолчанию событие срабатывает на открытие, переоткрытие и новые коммиты в ветке (opened, reopened, synchronize).
Проверки на pull request: Node.js, кэш, матрица, PostgreSQL
Workflow должен вызывать те же команды, что разработчик запускает локально, — тогда «у меня работает» и «в CI работает» означают одно и то же. Поэтому начинаем со скриптов в package.json:
{
"scripts": {
"lint": "oxlint --deny-warnings src tests",
"typecheck": "tsc -p .",
"test:unit": "vitest run --project unit",
"test:integration": "vitest run --project integration",
"test": "vitest run"
}
}Для линтера взят oxlint: проект использует TypeScript 7.0, а typescript-eslint 8.71 на момент проверки объявляет совместимость только с typescript <6.1.0. На TypeScript 5.x подойдёт привычная связка ESLint и typescript-eslint — для CI разницы нет, шаг всё равно вызывает npm run lint. Сам workflow:
name: CI
on:
pull_request:
push:
branches: [main]
workflow_call:
# Least privilege: the token can only read the code
permissions:
contents: read
# A new push to the same PR cancels the previous, now useless run
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
checks:
name: Lint and typecheck
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 24
cache: npm
- run: npm ci
- run: npm run lint
- run: npm run typecheck
unit:
name: Unit tests (Node ${{ matrix.node }})
runs-on: ubuntu-24.04
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
node: [22, 24, 26]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: ${{ matrix.node }}
cache: npm
# Node 22 ships npm 10, which rejects a lockfile written by npm 11
- run: npm install --global npm@11
- run: npm ci
- run: npm run test:unit
integration:
name: Integration tests
runs-on: ubuntu-24.04
timeout-minutes: 15
services:
postgres:
image: postgres:17-alpine
env:
POSTGRES_PASSWORD: test # a throwaway database: not a secret
POSTGRES_DB: leads_test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 5s
--health-timeout 5s
--health-retries 10
env:
TEST_DATABASE_URL: postgres://postgres:test@localhost:5432/leads_test
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 24
cache: npm
- run: npm ci
- run: npm run test:integration -- --reporter=default --reporter=junit --outputFile.junit=reports/integration.xml
- name: Upload test report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: integration-report
path: reports/
retention-days: 7
image:
name: Docker image
needs: [checks, unit, integration]
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with:
context: .
push: false
tags: leads-api:ci
# The single check to mark as required in branch protection
ci-ok:
name: CI passed
if: ${{ always() }}
needs: [checks, unit, integration, image]
runs-on: ubuntu-24.04
steps:
- name: Fail if any job failed or was cancelled
if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}
run: exit 1npm ciставит пакеты строго поpackage-lock.jsonи падает, если lock-файл не совпадает сpackage.json. В CI это то, что нужно: собирается ровно то, что проверено локально.cache: npmвsetup-nodeсохраняет глобальный кэш npm (~/.npm), а неnode_modules; ключ кэша — хэшpackage-lock.json. Пакеты не скачиваются из интернета заново, ноnpm ciвсё равно собираетnode_modulesс нуля.- Кэш, созданный в pull request, привязан к его merge-ref и доступен только перезапускам этого же pull request. Поэтому CI запускается и на
pushвmain: кэш веткиmainмогут восстановить все pull request в неё. - Матрица
node: [22, 24, 26]создаёт три заданияunit. По графику Node.js 22 поддерживается до 30 апреля 2027 года, 24 — LTS, 26 становится LTS 28 октября 2026 года.fail-fastпо умолчаниюtrue— при падении одной версии остальные отменяются; сfalseвы видите результат по всем версиям сразу. - Линтер и интеграционные тесты в матрицу не включены: их результат от версии Node.js почти не зависит, а время раннера стоит денег и минут лимита.
services.postgresподнимает PostgreSQL 17 рядом с заданием. Health-check (pg_isready) задерживает шаги, пока база не готова,portsпробрасывает порт на раннер — поэтому адресlocalhost:5432.TEST_DATABASE_URLнаправляетglobal-setup.tsиз урока о тестировании на эту базу вместо Testcontainers. Парольtest— не секрет: база живёт несколько минут и недоступна извне.- Отчёт JUnit загружается как artifact с условием
!cancelled(): и при упавших тестах — именно тогда он нужен, — но не при отмене прогона.retention-days: 7вместо 90 дней по умолчанию экономит хранилище. concurrencyсcancel-in-progressтолько дляpull_request: новый push в ветку отменяет устаревший прогон, а прогоны наmainдоводятся до конца.timeout-minutesважен: по умолчанию зависшее задание живёт 360 минут.
Строка npm install --global npm@11 в матрице появилась после первого же локального прогона. Lock-файл записан npm 11, а Node.js 22 поставляется с npm 10, который считает такой lock-файл несогласованным: в проекте msw 3, а необязательная peer-зависимость @vitest/mocker ожидает msw 2. На Node.js 24 и 26 тот же npm ci проходит. Реальный вывод задания на Node.js 22 до исправления:
[CI/Unit tests (Node 22)-1] ⭐ Run Main npm ci
[CI/Unit tests (Node 22)-1] | npm error code EUSAGE
[CI/Unit tests (Node 22)-1] | npm error `npm ci` can only install packages when your package.json and package-lock.json or npm-shrinkwrap.json are in sync. Please update your lock file with `npm install` before continuing.
[CI/Unit tests (Node 22)-1] | npm error Missing: msw@2.15.0 from lock file
[CI/Unit tests (Node 22)-1] | npm error Missing: @mswjs/interceptors@0.41.9 from lock file
…
[CI/Unit tests (Node 22)-1] ❌ Failure - Main npm ci [4.8119949s]
[CI/Unit tests (Node 22)-1] 🏁 Job failed
…
[CI/Unit tests (Node 24)-2] ✅ Success - Main npm run test:unit [1.3380536s]
[CI/Unit tests (Node 26)-3] ✅ Success - Main npm run test:unit [671.0905ms]Безопасность workflow: права, версии actions, недоверенный ввод
Workflow на pull_request выполняет код из этого pull request: всё, к чему у workflow есть доступ, доступно и этому коду. Поэтому правила из руководства GitHub «Secure use reference» стоит применять с первого файла, а не «когда-нибудь потом».
Минимальные права токена
GitHub выдаёт каждому заданию
GITHUB_TOKEN.permissions: contents: readна уровне workflow оставляет только чтение кода: если указано хотя бы одно право, все неуказанные становятсяnone. Расширяйте права точечно в задании — вrelease.ymlpackages: writeесть только у задания публикации.Actions по полному SHA
Тег вроде
@v7владелец репозитория action может передвинуть, и если репозиторий взломают, новая версия незаметно придёт во все workflow. Полный SHA коммита, по документации GitHub, — единственный способ сослаться на неизменяемую версию. Комментарий# v7.0.1— для людей. Чтобы закреплённые версии не устаревали, GitHub рекомендует Dependabot сpackage-ecosystem: github-actions— он следит за ссылками на actions в workflow. Берите SHA из репозитория самого action, а не из форка.Без лишних учётных данных
actions/checkoutпо умолчанию сохраняет токен, чтобы следующие шаги могли выполнять git-команды. Нашему CIgit pushне нужен, поэтомуpersist-credentials: false.Недоверенный ввод — только через env
Заголовок pull request, имя ветки, текст комментария пишет автор pull request. Выражение
${{ … }}подставляется в скрипт до запуска оболочки, поэтому заголовок с кавычками и;становится командой. Передавайте такие значения черезenvи используйте как обычную переменную.Осторожно с привилегированными событиями
pull_request_targetиworkflow_runвыполняются в контексте базового репозитория — с секретами и правом записи. Не скачивайте и не запускайте в них код из pull request;actions/checkoutначиная с v7 по умолчанию отказывается это делать.
name: PR title
on:
pull_request:
types: [opened, edited, synchronize, reopened]
permissions: {}
jobs:
title:
name: PR title format
runs-on: ubuntu-24.04
steps:
- name: Check PR title
env:
TITLE: ${{ github.event.pull_request.title }} # untrusted input: only via env
run: |
pattern='^(feat|fix|docs|chore): '
if [[ ! "$TITLE" =~ $pattern ]]; then
echo "PR title must start with feat:, fix:, docs: or chore:"
exit 1
fi
echo "PR title is OK"Мы прогнали этот workflow через act с заголовком a"; touch /tmp/pwned; echo ": проверка формата не прошла, а команда из заголовка не выполнилась — файла /tmp/pwned в контейнере нет. В контрольном варианте с ${{ github.event.pull_request.title }} прямо в run тот же заголовок создал файл. Секреты из Settings → Secrets and variables → Actions (или gh secret set NAME) читаются как ${{ secrets.NAME }}. Важно для CI: workflow, запущенный pull request из форка, секретов не получает (кроме GITHUB_TOKEN с правами только на чтение), а прогоны pull request от Dependabot работают как форки. Значит, проверки pull request должны обходиться без секретов — как наш ci.yml. Как хранить и ротировать секреты, разобрано в материале о секретах и переменных окружения.
Проверка до push: actionlint и локальный прогон через act
Ошибку в YAML GitHub покажет только после push — и пока вы её чините, история ветки обрастает коммитами «fix ci». actionlint — статический анализатор workflow: проверяет синтаксис, выражения ${{ }}, ссылки needs, типы контекстов, известные ловушки безопасности и прогоняет скрипты run через shellcheck. Проще всего запускать его в Docker из корня репозитория:
docker run --rm -v "$PWD:/repo" -w /repo rhysd/actionlint:1.7.12Вот что он находит в workflow с тремя типичными ошибками — опечаткой в имени переменной матрицы, заголовком pull request прямо в скрипте и ссылкой на несуществующее задание:
name: CI
on: pull_request
jobs:
unit:
runs-on: ubuntu-24.04
strategy:
matrix:
node: [22, 24]
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: ${{ matrix.nodes }}
- run: echo "Testing ${{ github.event.pull_request.title }}"
- run: npm ci && npm test
image:
needs: [checks]
runs-on: ubuntu-24.04
steps:
- run: docker build .$ docker run --rm -v "$PWD:/repo" -w /repo rhysd/actionlint:1.7.12
.github/workflows/ci.yml:15:29: property "nodes" is not defined in object type {node: number} [expression]
|
15 | node-version: ${{ matrix.nodes }}
| ^~~~~~~~~~~~
.github/workflows/ci.yml:16:32: "github.event.pull_request.title" is potentially untrusted. avoid using it directly in inline scripts. instead, pass it through an environment variable. see https://docs.github.com/en/actions/reference/security/secure-use#good-practices-for-mitigating-script-injection-attacks for more details [expression]
|
16 | - run: echo "Testing ${{ github.event.pull_request.title }}"
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.github/workflows/ci.yml:19:3: job "image" needs job "checks" which does not exist in this workflow [job-needs]
|
19 | image:
| ^~~~~~
$ echo $?
1actionlint не запускает задания. Для этого есть act — он читает те же файлы и выполняет задания в Docker-контейнерах, имитирующих раннер GitHub. Мы прогнали ci.yml на событии pull_request с образом catthehacker/ubuntu:act-24.04:
act pull_request -e pr-event.json \
-P ubuntu-24.04=catthehacker/ubuntu:act-24.04 \
--artifact-server-path /tmp/artifacts$ act pull_request -e pr-event.json \
-P ubuntu-24.04=catthehacker/ubuntu:act-24.04 \
--artifact-server-path /tmp/artifacts
…
[CI/Integration tests ] container health of 966c061c5bbf… (postgres:17-alpine) is healthy
…
[CI/Lint and typecheck ] ✅ Success - Main npm run lint [1.144679915s]
[CI/Lint and typecheck ] ✅ Success - Main npm run typecheck [1.023296871s]
[CI/Lint and typecheck ] 🏁 Job succeeded
[CI/Unit tests (Node 22)-1] | Tests 9 passed (9)
[CI/Unit tests (Node 22)-1] 🏁 Job succeeded
[CI/Unit tests (Node 26)-3] 🏁 Job succeeded
[CI/Unit tests (Node 24)-2] 🏁 Job succeeded
[CI/Integration tests ] | Tests 10 passed (10)
[CI/Integration tests ] | Artifact integration-report has been successfully uploaded! Final size is 750 bytes. Artifact ID is 3500521754
[CI/Integration tests ] 🏁 Job succeeded
[CI/Docker image ] ✅ Success - Main docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc [19.041554739s]
[CI/Docker image ] 🏁 Job succeeded
[CI/CI passed ] 🏁 Job succeededсинтаксис, выражения, needs, shellcheck
- Где проверяется
- actionlint — локально и в CI
шаги, матрица, npm ci, тесты, сервисный PostgreSQL, сборка образа, artifact
- Где проверяется
- act — локально, в Docker (с оговорками ниже)
права GITHUB_TOKEN, секреты, environment, защита ветки, публикация в ghcr.io
- Где проверяется
- только на GitHub: act подставляет свой токен и не знает настроек репозитория
Обязательные проверки: защита ветки
Сам по себе CI только сообщает результат: красный крестик не мешает нажать Merge. Блокирует слияние правило защиты ветки или ruleset с настройкой «Require status checks to pass before merging» — что ещё входит в защиту ветки, разобрано в материале о pull request. Вопрос в том, какие проверки сделать обязательными.
Каждое задание — отдельная проверка, у матрицы — по проверке на версию: Unit tests (Node 22), Unit tests (Node 24) и так далее. Перечислять их все неудобно: при добавлении версии в матрицу придётся менять настройки репозитория. Хуже другое: по документации GitHub, задание, зависящее от упавшего, пропускается, а статус пропущенного задания считается успешным — то есть обязательная проверка Docker image после падения тестов может не заблокировать слияние.
Поэтому в ci.yml есть итоговое задание ci-ok с именем CI passed. Оно зависит от всех остальных, выполняется всегда (if: ${{ always() }}) и падает, если хотя бы одно задание завершилось с failure или было отменено. Обязательной делается только эта проверка.
- Не вешайте на обязательный workflow фильтры
pathsиbranches: если workflow пропущен фильтром, его проверки остаются в состоянии «Pending» и блокируют слияние навсегда. - Имена заданий должны быть уникальны во всех workflow репозитория — одинаковые имена дают неоднозначный статус проверки.
- Строгий режим «Require branches to be up to date before merging» включён по умолчанию: ветку нужно обновить от base, и проверки идут заново. При большом потоке pull request ту же гарантию без ручного обновления даёт merge queue.
Публикация образа и деплой по тегу
Деплой запускается тегом версии, а не каждым слиянием: так в production попадает осознанно выпущенная версия, и всегда видно, какая именно. Образ собирается по Dockerfile из урока об основах Docker:
FROM node:24-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY src ./src
USER node
EXPOSE 3000
CMD ["node", "src/server.ts"]name: Release
on:
push:
tags: ['v*.*.*']
permissions:
contents: read
# Never run two deployments at once and never cancel one halfway
concurrency:
group: release-production
cancel-in-progress: false
jobs:
ci:
name: CI
uses: ./.github/workflows/ci.yml
publish:
name: Publish image
needs: ci
runs-on: ubuntu-24.04
timeout-minutes: 15
permissions:
contents: read
packages: write # push to ghcr.io with GITHUB_TOKEN
outputs:
digest: ${{ steps.push.outputs.digest }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: meta
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- id: push
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
deploy:
name: Deploy to production
needs: publish
runs-on: ubuntu-24.04
timeout-minutes: 10
# Environment secrets, required reviewers and allowed tags are set in Settings → Environments
environment:
name: production
url: https://leads.example.com
steps:
- name: Trigger the hosting deploy hook
env:
DEPLOY_HOOK_URL: ${{ secrets.DEPLOY_HOOK_URL }}
REPOSITORY: ${{ github.repository }}
DIGEST: ${{ needs.publish.outputs.digest }}
run: |
image="ghcr.io/${REPOSITORY,,}@${DIGEST}"
echo "Deploying $image"
curl --fail-with-body --silent --show-error -X POST "$DEPLOY_HOOK_URL" \
--data-urlencode "image=$image"uses: ./.github/workflows/ci.ymlвызывает CI как переиспользуемый workflow (для этого вci.ymlесть событиеworkflow_call) на том же коммите, что и тег. По документации, права токена в такой цепочке можно только сохранить или сузить, но не расширить.- Задание
publish— единственное сpackages: write:GITHUB_TOKENвходит вghcr.io,docker/metadata-actionстроит теги образа1.2.0и1.2из git-тега и приводит имя образа к нижнему регистру,build-push-actionсобирает и публикует образ и отдаёт его digest. environment: production— окружение из Settings → Environments. Его секреты, напримерDEPLOY_HOOK_URL, доступны только заданиям, которые на него ссылаются. Там же настраиваются обязательные ревьюеры деплоя и список тегов и веток, которым разрешено в него деплоить.- Деплой ссылается на образ по digest (
@sha256:…), а не по тегу: тег можно перезаписать, digest — нет.${REPOSITORY,,}в bash приводит имя репозитория к нижнему регистру, как того требует registry. concurrencyсcancel-in-progress: false: два выпуска не идут одновременно, и начатый деплой не обрывается на середине.
git switch main && git pull
git tag -a v1.2.0 -m "Lead source field"
git push origin v1.2.0Упражнения
Как проверено5 · пройдено3 · частичносверено 11 октября 2026 г.
- запущено, работает
.github/workflows/ci.yml, release.yml, pr-title.yml и решения упражнений (coverage-фрагмент в ci.yml, staging.yml)
actionlint 1.7.12 (docker rhysd/actionlint:1.7.12, со встроенным shellcheck 0.11.0) — 0 ошибок во всех файлах; SHA всех actions получены git ls-remote из репозиториев самих actions (lightweight-теги → коммиты)
- запущено, работает
находка: npm ci на Node 22 с lock-файлом npm 11
первый прогон act без шага npm@11: Node 22 (npm 10.9.9) — EUSAGE 'Missing: msw@2.15.0 from lock file', Node 24/26 — проходит; повторено в node:22-alpine: npm 10 ci exit=1, после npm install -g npm@11 (11.21.0) — exit=0. Причина: @vitest/mocker 5.0.3 объявляет optional peer msw ^2.4.9, в проекте msw 3.0.3
- запущено, работает
ci-ok (CI passed) — блокировка при упавшем задании
в прогоне act с несовместимым upload-artifact v7 задание Integration tests упало, Docker image не запускался (needs), CI passed выполнился и завершился exit 1; в первом прогоне аналогично при упавшем Node 22
- запущено, работает
pr-title.yml: недоверенный ввод через env
act с событием pull_request: заголовок 'feat: lead source field' — успех; заголовок 'a"; touch /tmp/pwned; echo "' — проверка формата падает, /tmp/pwned в контейнере задания отсутствует (act --reuse + docker exec ls); контрольный workflow с ${{ github.event.pull_request.title }} прямо в run — /tmp/pwned создан. Сломанный пример из статьи — реальный вывод actionlint (3 ошибки, exit 1)
- запущено, работает
package.json scripts, Dockerfile
проект из урока testirovanie-backenda + oxlint 1.87.0: npm run lint / typecheck (TypeScript 7.0.2) / test:unit на Windows и в act; docker build → образ 248 MB, запуск с фейковыми переменными (sk_test_FAKE_ci, FAKE_manager): node v24.20.0, пользователь node, GET /api/leads без токена → 401
- частично
ci.yml целиком — локальный прогон через act
act 0.2.89 (Linux-бинарь, sha256 сверен с checksums.txt) внутри контейнера catthehacker/ubuntu:act-24.04 с docker.sock, образ раннера catthehacker/ubuntu:act-24.04, событие pull_request из pr-event.json: все 7 заданий зелёные (lint+typecheck; unit 9/9 на Node 22.23.3, 24.21.0, 26.11.1; integration 10/10 против сервиса postgres:17-alpine; artifact integration-report.zip c integration.xml; сборка образа build-push-action; CI passed). Отличия копии для act: порт сервиса 55439 вместо 5432 (5432 на машине занят чужим контейнером, act ставит задания в сеть host) и upload-artifact v4.6.2 вместо v7.0.2 — сервер artifacts act 0.2.89 отвечает на v7 'unknown field mime_type' (проверено отдельным workflow: v4 загружается, v7 падает). Поведение на настоящем GitHub (permissions, кэш GitHub, merge-ref) не проверялось
- частично
release.yml: публикация образа и деплой
actionlint — 0 ошибок; шаг деплоя выполнен отдельно в контейнере против локального фейкового веб-хука: имя репозитория приводится к нижнему регистру (InterLab/Leads-API → interlab/leads-api), URL-кодированный image=…@sha256:… уходит в тело, ответ 403 → curl exit 22 (задание упадёт). Вход в ghcr.io, push образа, environment production и его секреты не проверялись — ничего на реальном GitHub не создавалось
- частично
упражнения
repeat: debugger; в src/leads.ts → oxlint 'eslint(no-debugger)' warning, --deny-warnings → exit 1; поведение статусов needs/always подтверждено прогонами act выше. modify: @vitest/coverage-v8 5.0.3, npm run test:unit -- --coverage создаёт coverage/ (html, clover, json), фрагмент проходит actionlint; уникальность имён artifact — по README upload-artifact, не прогонялась на GitHub. apply: staging.yml проходит actionlint, не запускался
«Запущено» — код выполнен во временном проекте или контейнере, вывод в статье взят из прогона. «Частично» — проверено с заглушками вместо внешних сервисов. «Не запускалось» — нужны устройство, аккаунт или ключ; шаги сверены с официальной документацией. Файлы проверочного проекта и логи запусков сохранены в архиве InterLAB.