惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

F
Fortinet All Blogs
WordPress大学
WordPress大学
The Cloudflare Blog
云风的 BLOG
云风的 BLOG
博客园 - Franky
D
Docker
小众软件
小众软件
阮一峰的网络日志
阮一峰的网络日志
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Engineering at Meta
Engineering at Meta
MongoDB | Blog
MongoDB | Blog
U
Unit 42
M
MIT News - Artificial intelligence
B
Blog
GbyAI
GbyAI
C
Check Point Blog
P
Proofpoint News Feed
博客园 - 司徒正美
Hugging Face - Blog
Hugging Face - Blog
雷峰网
雷峰网
IT之家
IT之家
Google DeepMind News
Google DeepMind News
V
V2EX
Stack Overflow Blog
Stack Overflow Blog

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Вечер на автоматизацию GitHub-ачивок. Что пошло не так (и...
Dmitry · 2026-05-29 · via DEV Community

Началось всё банально: хотел собрать ачивки на GitHub-профиле.

Процесс оказался несложным по сути, но раздражающим по исполнению — куча однотипных ручных шагов, ожидание ревьюеров, непрозрачные условия засчитывания. Типичная задача на «автоматизировать и забыть».

Я взял её как мини-проект на вечер. Вечер растянулся.


Что хотелось получить

Публичный репозиторий, куда любой может открыть PR, и он автоматически мёрджится — без мейнтейнера, без ожидания, без магии.

Звучит просто. Но сразу встал вопрос: как сделать это безопасно?

Открытый авто-мёрдж без ограничений — это либо спам, либо кто-то пропушит что-то лишнее. Нужна была система проверок.


Первое нетривиальное решение: pull_request_target

Для авто-мёрджа форков нужен доступ к секретам репозитория. Обычный pull_request такого доступа не даёт — он намеренно изолирован для безопасности.

Правильный ответ — pull_request_target. Он запускается в контексте целевого репозитория, а не форка, поэтому имеет доступ к токенам. Но это же делает его опасным, если не выстроить валидацию до любых действий.

on:
  pull_request_target:
    types: [opened, synchronize, reopened]
    paths:
      - "contributors/**"

Enter fullscreen mode Exit fullscreen mode

Вся логика валидации идёт первой, до мёрджа — иначе это дыра.


Валидация: динамический regex из логина автора

Я хотел, чтобы каждый пользователь мог добавить только свой файл. Не чужой, не случайный — именно 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));

Enter fullscreen mode Exit fullscreen mode

Паттерн допускает username.md, username-2.md, username-3.md — для случаев, когда нужно несколько PR от одного пользователя. Всё остальное — отказ с понятным сообщением прямо в комментарии к PR.


Galaxy Brain: правильный ответ спрятан в HTML-комментарии

Для ачивки Galaxy Brain нужно, чтобы кто-то ответил на вопрос в Discussions и ответ был помечен как правильный.

Автоматизировать проверку ответа — задача нетривиальная. Хранить правильные ответы в отдельном файле/базе? Лишние сущности. Сравнивать с внешним API в момент ответа? Медленно и ненадёжно.

Элегантное решение: правильный ответ прячется прямо в теле discussion в HTML-комментарии, который пользователь не видит, а workflow читает.

<!-- ANSWER: JavaScript -->

Enter fullscreen mode Exit fullscreen mode

Когда кто-то отвечает в треде, 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;
}

Enter fullscreen mode Exit fullscreen mode

Никакой внешней базы. Данные живут там, где они нужны.


Discussions API: только GraphQL

GitHub REST API не поддерживает принятие ответа в Discussions. Это доступно только через GraphQL — markDiscussionCommentAsAnswer.

const mutation = `
  mutation($commentId: ID!) {
    markDiscussionCommentAsAnswer(input: { id: $commentId }) {
      discussion { number url }
    }
  }
`;
await github.graphql(mutation, { commentId });

Enter fullscreen mode Exit fullscreen mode

Аналогично для создания самих вопросов и добавления реакций — всё через GraphQL. REST здесь просто не работает.


Идемпотентность: не создавать вопросы, пока есть неотвеченные

Workflow создания вопросов запускается три раза в день. Но создавать новые вопросы, пока старые висят без ответа — бессмысленно и засоряет Discussions.

Перед созданием — проверка:

const unanswered = result.repository.discussions.totalCount;

if (unanswered > 0) {
  // пропускаем этот запуск
  return;
}

Enter fullscreen mode Exit fullscreen mode

Запуск идемпотентен: повторный прогон не ломает состояние.


Схема всего

PR pipeline:

User opens PR
  → pull_request_target
  → Validation
      ✅ pass → Squash merge
      ❌ fail → Comment with reason

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode


Что получилось в итоге

Репозиторий, где:

  • 🦈 Pull Shark — PR в contributors/ → авто-мёрдж
  • 👥 Pair Extraordinaire — PR в pairs/ → авто-мёрдж с Co-authored-by
  • 🧠 Galaxy Brain — отвечай на вопросы в Discussions → бот принимает правильные
  • Quickdraw и 🤠 YOLO — инструкции за 2 клика

Без ожидания, без мейнтейнера, без магии.


Дисклеймер

Проект создан в образовательных и развлекательных целях.
Ачивки — косметика профиля, не метрика уровня инженера.

Используй в рамках GitHub Terms of Service.


Если идея понравилась — посмотри GitHub Achievement Farm и поставь ⭐, если зашло.

Если есть вопросы по деталям реализации или нашёл edge-case — welcome в issues.