
DmitryНачалось всё банально: хотел собрать ачивки на GitHub-профиле. Процесс оказался несложным по сути,...
Началось всё банально: хотел собрать ачивки на GitHub-профиле.
Процесс оказался несложным по сути, но раздражающим по исполнению — куча однотипных ручных шагов, ожидание ревьюеров, непрозрачные условия засчитывания. Типичная задача на «автоматизировать и забыть».
Я взял её как мини-проект на вечер. Вечер растянулся.
Публичный репозиторий, куда любой может открыть PR, и он автоматически мёрджится — без мейнтейнера, без ожидания, без магии.
Звучит просто. Но сразу встал вопрос: как сделать это безопасно?
Открытый авто-мёрдж без ограничений — это либо спам, либо кто-то пропушит что-то лишнее. Нужна была система проверок.
pull_request_target
Для авто-мёрджа форков нужен доступ к секретам репозитория. Обычный pull_request такого доступа не даёт — он намеренно изолирован для безопасности.
Правильный ответ — pull_request_target. Он запускается в контексте целевого репозитория, а не форка, поэтому имеет доступ к токенам. Но это же делает его опасным, если не выстроить валидацию до любых действий.
on:
pull_request_target:
types: [opened, synchronize, reopened]
paths:
- "contributors/**"
Вся логика валидации идёт первой, до мёрджа — иначе это дыра.
Я хотел, чтобы каждый пользователь мог добавить только свой файл. Не чужой, не случайный — именно username.md.
Жёсткий список заранее не составишь. Поэтому имя файла проверяется динамически: regex строится прямо из логина автора PR.
const author = context.payload.pull_request.user.login.toLowerCase();
const validPattern = new RegExp(`^contributors/${author}(-\\d+)?\\.md$`, 'i');
const invalidFiles = files.filter(f => !validPattern.test(f.filename));
Паттерн допускает username.md, username-2.md, username-3.md — для случаев, когда нужно несколько PR от одного пользователя. Всё остальное — отказ с понятным сообщением прямо в комментарии к PR.
Для ачивки Galaxy Brain нужно, чтобы кто-то ответил на вопрос в Discussions и ответ был помечен как правильный.
Автоматизировать проверку ответа — задача нетривиальная. Хранить правильные ответы в отдельном файле/базе? Лишние сущности. Сравнивать с внешним API в момент ответа? Медленно и ненадёжно.
Элегантное решение: правильный ответ прячется прямо в теле discussion в HTML-комментарии, который пользователь не видит, а workflow читает.
<!-- ANSWER: JavaScript -->
Когда кто-то отвечает в треде, workflow проверяет, содержит ли комментарий нужную строку:
const answerMatch = discussion.body.match(/<!--\s*ANSWER:\s*(.+?)\s*-->/i);
const correctAnswer = answerMatch[1].trim().toLowerCase();
const commentBody = context.payload.comment.body.toLowerCase();
if (!commentBody.includes(correctAnswer)) {
// неправильный ответ — ничего не делаем
return;
}
Никакой внешней базы. Данные живут там, где они нужны.
GitHub REST API не поддерживает принятие ответа в Discussions. Это доступно только через GraphQL — markDiscussionCommentAsAnswer.
const mutation = `
mutation($commentId: ID!) {
markDiscussionCommentAsAnswer(input: { id: $commentId }) {
discussion { number url }
}
}
`;
await github.graphql(mutation, { commentId });
Аналогично для создания самих вопросов и добавления реакций — всё через GraphQL. REST здесь просто не работает.
Workflow создания вопросов запускается три раза в день. Но создавать новые вопросы, пока старые висят без ответа — бессмысленно и засоряет Discussions.
Перед созданием — проверка:
const unanswered = result.repository.discussions.totalCount;
if (unanswered > 0) {
// пропускаем этот запуск
return;
}
Запуск идемпотентен: повторный прогон не ломает состояние.
PR pipeline:
User opens PR
→ pull_request_target
→ Validation
✅ pass → Squash merge
❌ fail → Comment with reason
Galaxy Brain pipeline:
Cron 3x/day
→ Unanswered questions exist?
yes → skip
no → fetch from OpenTDB
→ create discussions with hidden ANSWER tag
New discussion comment
→ correct answer? → markDiscussionCommentAsAnswer
→ wrong answer? → ignore
Репозиторий, где:
contributors/ → авто-мёрджpairs/ → авто-мёрдж с Co-authored-by
Без ожидания, без мейнтейнера, без магии.
Проект создан в образовательных и развлекательных целях.
Ачивки — косметика профиля, не метрика уровня инженера.
Используй в рамках GitHub Terms of Service.
Если идея понравилась — посмотри GitHub Achievement Farm и поставь ⭐, если зашло.
Если есть вопросы по деталям реализации или нашёл edge-case — welcome в issues.