Skip to content

Чинит автозаполнение ченджлога при слиянии пиара #10795

Чинит автозаполнение ченджлога при слиянии пиара

Чинит автозаполнение ченджлога при слиянии пиара #10795

Workflow file for this run

name: PR Preview
on:
pull_request_target:
jobs:
# Всё превью одной джобой: заметка, ожидание проверок и сборка. Отдельными
# джобами это давало пять галок в списке проверок пулреквеста, хотя интересен
# только исход.
preview:
name: Сборка и загрузка превью
runs-on: ubuntu-latest
# Заметку пишем в описание пулреквеста, проверки читаем через Checks API.
permissions:
pull-requests: write
checks: read
contents: read
outputs:
checks: ${{ steps.gate.outcome }}
published: ${{ steps.build-preview.outcome == 'success' }}
links: ${{ steps.links.outputs.list }}
env:
DEPLOY_DOMAIN: https://content-${{ github.event.pull_request.number }}.dev.doka.guide
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PATH_TO_CONTENT: ./content
steps:
# Заметка живёт в описании пулреквеста, а не отдельным комментарием: GitHub
# шлёт письмо на создание комментария, а правку описания не рассылает вовсе.
- name: Заметка о начале
uses: actions/github-script@v7
env:
MESSAGE: "Идут проверки и сборка превью... [Подробнее](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})"
with:
script: |
const { owner, repo } = context.repo
const pull_number = context.payload.pull_request.number
const marker = `<!-- doka-preview-${pull_number} -->`
const endMarker = `<!-- /doka-preview-${pull_number} -->`
const block = `${marker}\n${process.env.MESSAGE}\n${endMarker}`
// Описание перечитываем прямо перед записью: автор мог поправить
// его, пока шли проверки и сборка.
const { data: pull } = await github.rest.pulls.get({ owner, repo, pull_number })
const current = pull.body || ''
const start = current.indexOf(marker)
const end = current.indexOf(endMarker)
const body =
start !== -1 && end !== -1 && end > start
? current.slice(0, start) + block + current.slice(end + endMarker.length)
: current.trimEnd()
? `${current.trimEnd()}\n\n${block}`
: block
await github.rest.pulls.update({ owner, repo, pull_number, body })
# Гейт: превью не публикуется, пока проверки пулреквеста не зелёные.
# Проверки живут в отдельном воркфлоу (PR Checks) на триггере pull_request,
# а needs между воркфлоу не работает — поэтому опрашиваем Checks API.
# Переносить сами проверки сюда нельзя: они выполняют код из форка, а под
# pull_request_target у GITHUB_TOKEN права на запись.
- name: Ожидание проверок
id: gate
uses: actions/github-script@v7
with:
script: |
const names = ['Форматирование', 'Мета', 'Ссылки']
const { owner, repo } = context.repo
const ref = context.payload.pull_request.head.sha
const deadline = Date.now() + 15 * 60 * 1000
while (Date.now() < deadline) {
const { data } = await github.rest.checks.listForRef({ owner, repo, ref, per_page: 100 })
const runs = names.map((name) => data.check_runs.find((run) => run.name === name))
// skipped и neutral провалом не считаются: «Ссылки» пропускаются,
// когда упали предыдущие проверки, и их провал уже виден отдельно.
const failed = runs.filter(
(run) =>
run &&
run.status === 'completed' &&
!['success', 'skipped', 'neutral'].includes(run.conclusion),
)
if (failed.length) {
core.setFailed(`Проверки не прошли: ${failed.map((run) => run.name).join(', ')}`)
return
}
if (runs.every((run) => run && run.status === 'completed')) {
core.info('Все проверки пройдены')
return
}
const pending = names.filter((name, index) => !runs[index] || runs[index].status !== 'completed')
core.info(`Ждём: ${pending.join(', ')}`)
await new Promise((resolve) => setTimeout(resolve, 15000))
}
core.setFailed('Проверки не завершились за 15 минут')
# Собирает адреса страниц, затронутых пулреквестом. Соответствие пути
# и адреса разное у разных разделов:
# css/optional/index.md → /css/optional/
# pages/about/index.md → /about/ папка раздела выпадает
# interviews/…/index.md → страницы нет, у вопросов её не бывает
# Демки, картинки, советы «На практике» и ответы лежат глубже и своих
# страниц тоже не имеют, поэтому в список не попадают.
- name: Выборка изменённых материалов
id: files
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -eu
urls=$(gh api --paginate \
"repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files" \
--jq '.[].filename' \
| awk -F/ '
NF == 3 && $3 == "index.md" {
if ($1 ~ /^(html|css|js|a11y|tools|recipes|people)$/) print "/" $1 "/" $2 "/"
else if ($1 ~ /^(pages|specials)$/) print "/" $2 "/"
}' \
| sort -u \
| tr '\n' ' ')
echo "urls=$urls" >> "$GITHUB_OUTPUT"
echo "Затронуто страниц: $(echo $urls | wc -w)"
- name: Загрузка platform
uses: actions/checkout@v4
with:
repository: doka-guide/platform
- name: Загрузка контента
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
path: content
allow-unsafe-pr-checkout: true
- name: Загрузка кеша
uses: actions/checkout@v4
with:
repository: doka-guide/cache
path: cache
# Версия берётся из .nvmrc платформы: именно её зависимости здесь
# ставятся. Зашитая версия разъезжается с платформой — так превью и
# выкатка сломались, когда платформа перешла на Node 24, а здесь
# осталась двадцатая.
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
- name: Установка модулей
run: npm ci
- name: Копирование кеша
run: cp ./cache/issues.json ./.issues.json
# Блок со ссылками добавляется к сообщению только когда есть что
# показать: у пулреквестов, которые правят воркфлоу или служебные файлы,
# затронутых материалов нет, и раскрывашка там пустая и бессмысленная.
- name: Формирование ссылок на материалы
id: links
run: |
set -eu
items=""
for url in ${{ steps.files.outputs.urls }}; do
items="${items}<li><a href=\"${{ env.DEPLOY_DOMAIN }}${url}\">${url}</a></li>"
done
if [ -n "$items" ]; then
block="<details><summary>Затронутые материалы</summary><ul>${items}</ul></details>"
else
block=""
fi
echo "list=$block" >> "$GITHUB_OUTPUT"
- name: Установка ключа для пользователя
run: |
set -eu
mkdir "$HOME/.ssh"
echo "${{ secrets.DEPLOY_KEY }}" > "$HOME/.ssh/doka_deploy"
chmod 600 "$HOME/.ssh/doka_deploy"
- name: Сборка и публикация сайта
id: build-preview
continue-on-error: true
run: |
set -eu
cp .env.example .env
# Сборка молчит около двух минут, а публикация — ещё дольше. Без
# отметок шаг выглядит зависшим, и непонятно, на чём именно он стоит.
echo "::group::Сборка сайта"
echo "Начало: $(date -u +%H:%M:%S) UTC. Сборка занимает пару минут и молчит до конца."
npm run build
echo "Готово: $(date -u +%H:%M:%S) UTC. Страниц собрано: $(find dist -name '*.html' | wc -l)."
echo "::endgroup::"
echo "::group::Публикация превью"
echo "Начало: $(date -u +%H:%M:%S) UTC. Отправляется $(du -sh dist | cut -f1)."
ssh -i $HOME/.ssh/doka_deploy -o StrictHostKeyChecking=no deploy@dev.doka.guide mkdir -p /web/sites/dev.doka.guide/content/${{ github.event.pull_request.number }}
# --info=progress2 показывает общий прогресс одной обновляемой строкой,
# а не портянку из десятков тысяч имён файлов, как прежний --progress.
cd dist && rsync -e "ssh -i $HOME/.ssh/doka_deploy -o StrictHostKeyChecking=no" \
--archive --compress --delete --info=progress2 \
. deploy@dev.doka.guide:/web/sites/dev.doka.guide/content/${{ github.event.pull_request.number }}
echo ""
echo "Готово: $(date -u +%H:%M:%S) UTC."
echo "::endgroup::"
echo "Ссылка на превью — ${{ env.DEPLOY_DOMAIN }}"
# Итоговая заметка. always(), иначе при упавших проверках или сборке в
# описании навсегда останется «Идут проверки и сборка...».
report:
name: Отчёт в пулреквесте
runs-on: ubuntu-latest
timeout-minutes: 5
needs:
- preview
if: ${{ always() && !cancelled() }}
permissions:
pull-requests: write
env:
DEPLOY_DOMAIN: https://content-${{ github.event.pull_request.number }}.dev.doka.guide
steps:
- uses: actions/github-script@v7
env:
# Обращение по индексу, а не через точку: в идентификаторе джобы есть
# дефис, и выражение разобралось бы как вычитание.
CHECKS_PASSED: ${{ needs.preview.outputs.checks == 'success' }}
IS_PUBLISHED: ${{ needs.preview.outputs.published == 'true' }}
LINKS: ${{ needs.preview.outputs.links }}
RUN_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
DEPLOY_DOMAIN: ${{ env.DEPLOY_DOMAIN }}
with:
script: |
const checksPassed = process.env.CHECKS_PASSED === 'true'
const isPublished = process.env.IS_PUBLISHED === 'true'
const sha = process.env.HEAD_SHA
let message
if (!checksPassed) {
message = `Превью контента из ${sha} не опубликовано: не прошли проверки пулреквеста. [Подробнее](${process.env.RUN_URL})`
} else if (isPublished) {
message = `<a href="${process.env.DEPLOY_DOMAIN}">Превью контента</a> из ${sha} опубликовано.${process.env.LINKS || ''}`
} else {
message = `Превью контента из ${sha} не опубликовано. Ошибка сборки или публикации. [Подробнее](${process.env.RUN_URL})`
}
const { owner, repo } = context.repo
const pull_number = context.payload.pull_request.number
const marker = `<!-- doka-preview-${pull_number} -->`
const endMarker = `<!-- /doka-preview-${pull_number} -->`
const block = `${marker}\n${message}\n${endMarker}`
// Описание перечитываем прямо перед записью: автор мог поправить
// его, пока шли проверки и сборка.
const { data: pull } = await github.rest.pulls.get({ owner, repo, pull_number })
const current = pull.body || ''
const start = current.indexOf(marker)
const end = current.indexOf(endMarker)
const body =
start !== -1 && end !== -1 && end > start
? current.slice(0, start) + block + current.slice(end + endMarker.length)
: current.trimEnd()
? `${current.trimEnd()}\n\n${block}`
: block
await github.rest.pulls.update({ owner, repo, pull_number, body })
// Красная галка нужна только на нашей поломке: провал проверок уже
// виден отдельной красной джобой, дублировать его незачем.
if (checksPassed && !isPublished) {
core.setFailed('Превью не опубликовано')
}
# Комментарии прошлой схемы. Убираем оставшиеся, чтобы в ленте не висело
# вечное «Идёт сборка...». Шаг можно удалить, когда их не останется.
- name: Уборка старых комментариев
uses: actions/github-script@v7
continue-on-error: true
with:
script: |
const { owner, repo } = context.repo
const issue_number = context.payload.pull_request.number
const comments = await github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number,
per_page: 100,
})
for (const comment of comments) {
const isBot = comment.user?.login === 'github-actions[bot]'
if (isBot && (comment.body || '').includes('Превью контента')) {
await github.rest.issues.deleteComment({ owner, repo, comment_id: comment.id })
}
}