В долгосрочных проектах накапливаются устаревшие задачи: баги для старых версий, фичи, которые уже не актуальны, тестовые тикеты. Ручное удаление - долго и скучно. Этот скрипт автоматически находит и удаляет (или помечает) старые задачи по заданным меткам.

💡 Скрипт использует dry-run по умолчанию - сначала покажет, что будет удалено, без реального удаления.


📦 Скрипт: delete-issues.sh

Полный код

#!/bin/bash
# Удаление старых GitHub Issues по меткам и дате
# Использование: ./delete-issues.sh [--execute]

set -euo pipefail

# === НАСТРОЙКИ ===
REPO="owner/repo"                          # Репозиторий в формате owner/repo
LABELS='label1,label2,label3'              # Метки для фильтрации (через запятую)
CUTOFF="2025-12-31T23:59:59Z"             # Удалять задачи, созданные ДО этой даты
DRY_RUN=true                               # true = только показать, false = реально удалить

# Парсинг аргументов
if [[ "${1:-}" == "--execute" ]]; then
    DRY_RUN=false
    echo "⚠️  Режим: РЕАЛЬНОЕ УДАЛЕНИЕ"
else
    echo "ℹ️  Режим: DRY RUN (ничего не будет удалено)"
fi

echo "🔍 Поиск задач в $REPO с метками: $LABELS, созданных до $CUTOFF"
echo "---"

# Получение и фильтрация задач
gh issue list --repo "$REPO" --state all --limit 1000 \
  --json number,title,createdAt,labels,url | \
  jq -r --arg labels "$LABELS" --arg cutoff "$CUTOFF" '
    ($labels | split(",")) as $label_array |
    .[] |
    select(.createdAt < $cutoff) |
    select(.labels | map(.name) | any(. as $l | $label_array | index($l))) |
    "\(.number)|\(.title)|\(.url)"
  ' | \
  while IFS='|' read -r number title url; do
    if [[ "$DRY_RUN" == "true" ]]; then
      echo "[DRY RUN] ##$number - $title"
      echo "          $url"
    else
      echo "🗑  Удаление ##$number - $title"
      gh issue delete "$REPO" "$number" --yes
      sleep 1  # Небольшая пауза, чтобы не превысить rate limit
    fi
  done

echo "---"
echo "✅ Готово"

Установка зависимостей

# GitHub CLI
# Windows (winget):
winget install GitHub.cli

# Linux (Ubuntu/Debian):
sudo apt install gh

# macOS (Homebrew):
brew install gh

# Авторизация
gh auth login

# jq (JSON-процессор)
# Windows (winget):
winget install jq.jq

# Linux:
sudo apt install jq

# macOS:
brew install jq

Запуск

# Сделать скрипт исполняемым
chmod +x delete-issues.sh

# DRY RUN (безопасный режим - только показать)
./delete-issues.sh

# РЕАЛЬНОЕ УДАЛЕНИЕ (добавить флаг --execute)
./delete-issues.sh --execute

🔍 Как это работает

Пошаговый разбор

ШагКомандаЧто делает
1gh issue list --json ...Получает задачи в формате JSON с полями: номер, заголовок, дата, метки, ссылка
2jq -r ...Фильтрует: дата < cutoff И есть хотя бы одна из указанных меток
3while read ...Обрабатывает каждую найденную задачу
4gh issue deleteУдаляет задачу (только если DRY_RUN=false)

Логика jq-фильтра

($labels | split(",")) as $label_array |  # Разбиваем строку меток в массив
.[] |                                      # Для каждой задачи
select(.createdAt < $cutoff) |            # Только если создана до cutoff
select(
  .labels | map(.name) |                  # Получаем список имён меток
  any(. as $l | $label_array | index($l)) # Есть ли совпадение с нашими метками
) |
"\(.number)|\(.title)|\(.url)"            # Форматируем вывод

Почему так:

  • split(",") - позволяет задавать метки одной строкой
  • any(...) - проверяет наличие любой из указанных меток (логическое ИЛИ)
  • Формат вывода через | - надёжный способ передать заголовок с пробелами

⚙️ Адаптация под свои задачи

Пример 1: Удалить старые баги

REPO="myorg/myproject"
LABELS='bug,critical'
CUTOFF="2024-01-01T00:00:00Z"  # Удалить баги до 2024 года

Пример 2: Архивировать, а не удалять

Замените блок удаления на закрытие:

# Вместо gh issue delete:
echo "🔒 Закрытие ##$number - $title"
gh issue edit "$REPO" "$number" --state closed

Пример 3: Экспорт перед удалением (резервная копия)

# Добавить перед удалением:
echo "💾 Экспорт ##$number"
gh issue view "$REPO" "$number" --json title,body,comments >> backup-issues.jsonl

🛡 Безопасность

Обязательные проверки перед запуском

# 1. Проверить, что выбранные задачи - действительно те, что нужно
./delete-issues.sh | grep -c "\[DRY RUN\]"   # Посчитать количество

# 2. Убедиться, что в списке нет важных задач
./delete-issues.sh | grep -i "critical\|important"

# 3. Проверить права доступа
gh api /repos/$REPO | jq '.permissions.admin'  # Должно быть true

Rate limits GitHub API

ТипЛимитКак не превысить
Авторизованный5000 запросов/часsleep 1 между удалениями
Без авторизации60 запросов/часВсегда использовать gh auth login

Проверить остаток лимита:

gh api rate_limit | jq '.resources.core'

📊 Альтернативы и расширения

Только показать статистику (без удаления)

gh issue list --repo "$REPO" --state all --limit 1000 \
  --json createdAt,labels | \
  jq -r --arg labels "$LABELS" --arg cutoff "$CUTOFF" '
    ($labels|split(",")) as $label_array |
    [.[] | select(.createdAt < $cutoff) |
     select(.labels | map(.name) | any(. as $l | $label_array | index($l)))] |
    length
  '

Удаление по автору + меткам

select(.author.login == "bot-user") |
select(.labels | map(.name) | any(. as $l | $label_array | index($l)))

Экспорт в CSV перед удалением

# Добавить в jq-вывод:
"\(.number),\"\(.title)\",\(.createdAt),\(.url)"

⚠️ Частые проблемы

# "gh: command not found"
→ Установить GitHub CLI: https://cli.github.com

# "jq: error: syntax error"
→ Проверить кавычки в команде: использовать одинарные для jq, двойные для bash

# "HTTP 404: Not Found"
→ Проверить имя репозитория: должно быть owner/repo
→ Убедиться, что у токена есть права на чтение issues

# "rate limit exceeded"
→ Подождать сброса лимита или добавить задержку: sleep 2

# Удалил не то!
→ Всегда запускать сначала без --execute
→ Делать бэкап: gh issue list --json ... > backup.json

🗂 Чеклист перед запуском

  • Установил gh и jq, выполнил gh auth login
  • Проверил права на репозиторий: gh api /repos/owner/repo
  • Протестировал в dry-run режиме, проверил список задач
  • Убедился, что в списке нет критичных задач
  • При необходимости - сделал экспорт: gh issue list --json ... > backup.json
  • Только после этого - запуск с --execute

Ссылки