このランブックは、経過日数とステータスをもとに放置された解決済みの Jira 課題を探し、見つかったものすべての CSV レポートを生成したうえで、必要に応じてラベル付けやコンポーネントの更新を一括で行います。プロジェクトを定期的に整理するためのもので、毎月実行すればバックログをきれいに保てます。
atlassian-cli がインストールされていること(インストールガイド)atlassian-cli auth login で Jira の認証が設定されていることjq がインストールされていること# Preview issues resolved more than 180 days ago
./project-cleanup.sh --project PROJ --days 180 --dry-run
# Label old issues as archived
./project-cleanup.sh --project PROJ --days 180 --label "archived"
# Update component for all Done issues
./project-cleanup.sh --project PROJ --status Done --component "Legacy"
#!/bin/bash
# Jira Project Cleanup Script
#
# Performs bulk cleanup operations on Jira issues:
# - Archive old resolved issues
# - Update labels for categorization
# - Update components or fix versions
#
# Usage:
# # Dry run to preview changes
# ./project-cleanup.sh --project PROJ --days 180 --dry-run
#
# # Add archived label to old issues
# ./project-cleanup.sh --project PROJ --days 180 --label archived
#
# # Bulk update component
# ./project-cleanup.sh --project PROJ --status Done --component Legacy
#
# Requirements:
# - atlassian-cli installed and configured
set -euo pipefail
# Configuration
PROJECT=""
DAYS_OLD=""
STATUS=""
LABEL=""
COMPONENT=""
PROFILE="default"
DRY_RUN=true
CONCURRENCY=4
# Colors
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m'
log() {
echo -e "${GREEN}[INFO]${NC} $*"
}
warn() {
echo -e "${YELLOW}[WARN]${NC} $*"
}
error() {
echo -e "${RED}[ERROR]${NC} $*" >&2
}
# Parse arguments
parse_args() {
while [[ $# -gt 0 ]]; do
case $1 in
--project)
PROJECT="$2"
shift 2
;;
--days)
DAYS_OLD="$2"
shift 2
;;
--status)
STATUS="$2"
shift 2
;;
--label)
LABEL="$2"
DRY_RUN=false
shift 2
;;
--component)
COMPONENT="$2"
DRY_RUN=false
shift 2
;;
--profile)
PROFILE="$2"
shift 2
;;
--dry-run)
DRY_RUN=true
shift
;;
*)
error "Unknown option: $1"
exit 1
;;
esac
done
if [ -z "$PROJECT" ]; then
error "Project key required (--project PROJECT)"
exit 1
fi
}
# Build JQL query
build_jql() {
local jql="project = $PROJECT"
if [ -n "$DAYS_OLD" ]; then
local cutoff_date
cutoff_date=$(date -u -d "$DAYS_OLD days ago" +%Y-%m-%d 2>/dev/null || \
date -u -v-${DAYS_OLD}d +%Y-%m-%d) # macOS fallback
jql="$jql AND resolved < -${DAYS_OLD}d"
fi
if [ -n "$STATUS" ]; then
jql="$jql AND status = \"$STATUS\""
fi
echo "$jql"
}
# Preview affected issues
preview_issues() {
local jql="$1"
log "Finding issues matching criteria..."
log "JQL: $jql"
local issues
issues=$(atlassian-cli jira issue search \
--profile "$PROFILE" \
--jql "$jql" \
--output json 2>/dev/null || echo "[]")
local count
count=$(echo "$issues" | jq '. | length')
if [ "$count" -eq 0 ]; then
warn "No issues found matching criteria"
return 1
fi
log "Found $count issues to process"
# Show sample issues
echo ""
echo "Sample issues (first 10):"
echo "$issues" | jq -r '.[:10][] | " - \(.key): \(.fields.summary) (\(.fields.status.name))"'
echo ""
return 0
}
# Add label to issues
add_label() {
local jql="$1"
local label="$2"
if [ "$DRY_RUN" = "true" ]; then
warn "[DRY-RUN] Would add label '$label' to matching issues"
return
fi
log "Adding label '$label' to issues..."
atlassian-cli jira bulk label \
--profile "$PROFILE" \
--jql "$jql" \
--labels "$label" \
--concurrency "$CONCURRENCY"
log "Labels added successfully"
}
# Update component
update_component() {
local jql="$1"
local component="$2"
if [ "$DRY_RUN" = "true" ]; then
warn "[DRY-RUN] Would update component to '$component' for matching issues"
return
fi
warn "Bulk component update requires custom implementation"
warn "Use: atlassian-cli jira issue update <key> --component \"$component\""
}
# Generate cleanup report
generate_report() {
local jql="$1"
log "Generating cleanup report..."
local issues
issues=$(atlassian-cli jira issue search \
--profile "$PROFILE" \
--jql "$jql" \
--output json)
local report_file="cleanup_report_$(date +%Y%m%d_%H%M%S).csv"
echo "Issue Key,Summary,Status,Created,Resolved,Labels" > "$report_file"
echo "$issues" | jq -r '.[] |
[
.key,
(.fields.summary | gsub("\"";"\"\"") ),
.fields.status.name,
.fields.created,
(.fields.resolutiondate // "N/A"),
((.fields.labels // []) | join(";"))
] | @csv' >> "$report_file"
log "Report saved: $report_file"
}
# Main execution
main() {
parse_args "$@"
log "Jira Project Cleanup"
log "Project: $PROJECT | Profile: $PROFILE"
if [ "$DRY_RUN" = "true" ]; then
warn "DRY-RUN MODE: No changes will be made"
fi
local jql
jql=$(build_jql)
if ! preview_issues "$jql"; then
exit 0
fi
# Generate report
generate_report "$jql"
# Execute operations
if [ -n "$LABEL" ]; then
add_label "$jql" "$LABEL"
fi
if [ -n "$COMPONENT" ]; then
update_component "$jql" "$COMPONENT"
fi
log "Cleanup complete"
}
main "$@"
JQL を動的に組み立てる。 スクリプトは、指定したフラグから JQL クエリを組み立てます。--project でプロジェクトキーを指定し、--days で解決から N 日以上経過した課題に絞り込み、--status でステータスの条件を追加します。これらは 1 つのクエリにまとめられます。
一致する課題をプレビューする。 変更を加える前に、スクリプトが Jira を検索し、一致した最初の 10 件を課題キー、要約、現在のステータスとあわせて表示します。一致する課題がなければ、そのまま正常に終了します。
CSV レポートを生成する。 実行のたびにタイムスタンプ付きの CSV ファイル(cleanup_report_YYYYMMDD_HHMMSS.csv)が作成され、課題キー、要約、ステータス、作成日、解決日、ラベルが記録されます。これが監査証跡になります。
ラベルを一括で付ける。 --label を指定すると、スクリプトは CLI の bulk label コマンドを使って、一致したすべての課題に同時にラベルを付けます。よくある使い方は、古い課題に archived を付けることです。
コンポーネントの更新。 --component フラグはコンポーネントの付け替えを対象とします。コンポーネントの一括更新には課題ごとの API 呼び出しが必要なため、スクリプトは自分のワークフローに組み込むためのコマンドのひな形を提示します。