[#71395] Create a tool to populate Jira with projects and issues

https://community.openproject.org/work_packages/71395
This commit is contained in:
as-op
2026-02-05 11:19:13 +01:00
parent 6d161df36a
commit bc7a4aa73a
+886
View File
@@ -0,0 +1,886 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
# Script to mass-create projects and issues in a Jira Data Center server for testing.
#
# Usage:
# ruby script/jira/gen_jira_projects.rb [options]
#
# Options:
# --projects N Number of projects to create (default: 1000)
# --min-issues N Minimum issues per project (default: 10)
# --max-issues N Maximum issues per project (default: 100)
# --dry-run Show what would be created without making API calls
# --help Show this help message
#
# Environment variables (in .env file):
# JIRA_URL Jira Data Center URL (e.g., https://jira.example.com)
# JIRA_TOKEN Personal Access Token for authentication
require "bundler/setup"
require "active_support/core_ext/object/blank"
require "active_support/core_ext/enumerable"
require "colored2"
require "httpx"
require "json"
require "optparse"
require "securerandom"
# rubocop:disable Metrics/CollectionLiteralLength, Metrics/PerceivedComplexity, Metrics/AbcSize
class JiraProjectCreator
ISSUE_TYPES = %w[Task Bug Story Epic].freeze
PRIORITIES = %w[Highest High Medium Low Lowest].freeze
PROJECT_TEMPLATES = %w[
com.pyxis.greenhopper.jira:gh-scrum-template
com.pyxis.greenhopper.jira:gh-kanban-template
com.pyxis.greenhopper.jira:basic-software-development-template
].freeze
PROJECT_CATEGORIES = [
{ name: "Frontend", description: "User interface and client-side projects" },
{ name: "Backend", description: "Server-side and API projects" },
{ name: "Infrastructure", description: "DevOps, CI/CD, and platform projects" },
{ name: "Mobile", description: "iOS and Android applications" },
{ name: "Data", description: "Analytics, ML, and data pipeline projects" },
{ name: "Security", description: "Security and compliance projects" },
{ name: "Platform", description: "Core platform and shared services" },
{ name: "Experiments", description: "R&D and proof of concept projects" },
{ name: "Legacy", description: "Maintenance and migration projects" },
{ name: "Integrations", description: "Third-party integrations and APIs" },
{ name: "Internal Tools", description: "Developer tooling and automation" },
{ name: "Customer Success", description: "Customer-facing improvements" }
].freeze
PROJECT_ADJECTIVES = %w[
Alpha Beta Gamma Delta Omega Phoenix Quantum Nova Solar Lunar
Stellar Cosmic Rapid Swift Agile Smart Digital Cloud Native
Modern Legacy Core Prime Elite Ultra Hyper Mega Super Turbo
Atomic Dynamic Fusion Global Infinite Neural Optical Prism
Radiant Shadow Titan Vector Vertex Zenith Aurora Blaze Cipher
Crystal Diamond Echo Ember Falcon Glacier Harbor Horizon Iron
Jade Kinetic Liberty Marble Neptune Onyx Pacific Quartz Raven
Sapphire Thunder Unity Vortex Wildfire Xenon Yeti Zephyr Amber
Bronze Cobalt Crimson Emerald Frost Golden Indigo Jasper Neon
Obsidian Pearl Ruby Scarlet Silver Topaz Violet Copper Coral
Azure Cerulean Ivory Slate Storm Arctic Boreal Evergreen Summit
Pioneer Venture Apex Astral Bright Central Cyber Edge Frontier
Ignite Impulse Lunar Micro Nano Nitro Polar Presto Pulse Sonic
Spark Sprint Steady Stream Surge Synergy Terra Thrust Titan Trek
Vital Wave Zero Nexus Orbit Pinnacle Precision Proton Radix
Chunky Fluffy Wobbly Sneaky Dizzy Fuzzy Grumpy Spicy Crispy Crunchy
Bouncy Giggly Wiggly Bubbly Sparkly Twisty Zippy Zesty Snappy Peppy
Mighty Squishy Sleepy Groovy Funky Wacky Quirky Nerdy Geeky Fancy
Cosmic Mystic Magic Ninja Pirate Robot Zombie Viking Wizard Dragon
Caffeine Turbo Rocket Laser Plasma Stealth Ninja Secret Covert Hidden
Overdue Urgent Blocking Critical Legendary Epic Mythic Heroic Supreme
Confused Bewildered Puzzled Random Chaotic Mysterious Cryptic Obscure
Sloppy Messy Untidy Cluttered Disorganized Haphazard Jumbled Scattered
Hangry Salty Caffeinated Decaf Debugging Compiling Deploying Refactored
Botched Hacked Glitched Crashed Frozen Laggy Janky Wonky
Careless Reckless Fearless Witty Quirky
].freeze
PROJECT_NOUNS = %w[
Platform System Portal Gateway Engine Framework Module Suite
Hub Network Bridge Pipeline Toolkit Factory Studio Workspace
Tracker Manager Console Dashboard Registry Vault Nexus Matrix
Accelerator Analyzer Architect Archive Beacon Catalyst Chamber
Compass Conduit Controller Curator Depot Director Dispatcher
Dome Endpoint Explorer Forge Frontier Generator Guardian Harvester
Incubator Inspector Integrator Junction Kernel Launchpad
Lighthouse Link Mainframe Monitor Navigator Observatory Operator
Oracle Orchestrator Outpost Pavilion Processor Prototype Radar
Reactor Relay Repository Resolver Router Sanctuary Scheduler
Sentinel Server Shield Shuttle Signal Simulator Socket Sphere
Station Streamer Switchboard Terminal Tower Transmitter Tunnel
Warehouse Watcher Workshop Zone Amplifier Basecamp Command
Datastore Exchange Grid Index Ledger Mesh Node Pathway Pulse
Cluster Core Cortex Database Dock Domain Drive Dynamo Ecosystem
Element Emitter Encoder Entity Express Extractor Fabric Fleet
Flow Focus Foundry Frame Funnel Hive Interface Inventory Lab
Layer Library Loader Locus Logic Loop Machine Map Marker Memory
Mill Mind Mirror Mover Multiplexer Nerve Ocean Office Optimizer
Panel Parser Partner Pattern Planner Plugin Point Pool Port
Powerhouse Producer Profile Projector Provider Queue Ranch Range
Reader Receiver Recorder Refinery Register Repeater Resource Ring
Scale Scanner Scope Screen Seeker Segment Sequencer Service Shop
Silo Site Slice Source Space Spectrum Spire Spot Stack Stage
Store Strategy Structure Studio Surface Switch Table Tank Target
Template Thread Tier Timer Token Tool Track Trail Transform Tree
Trunk Unit Utility Valve Vessel View Viewer Vision Warehouse Web
Unicorn Narwhal Llama Penguin Panda Octopus Kraken Phoenix Dragon
Waffle Pancake Taco Burrito Pretzel Donut Bagel Cookie Muffin Pizza
Mongoose Hamster Badger Otter Koala Sloth Platypus Capybara Wombat
Dumpster Trashcan Landfill Bonfire Trainwreck Rollercoaster Circus
Spaghetti Lasagna Noodle Pickle Banana Avocado Coconut Pineapple
Thunderdome Treehouse Clubhouse Bunker Fortress Castle Dungeon Lair
Contraption Gizmo Widget Gadget Doodad Thingamajig Whatchamacallit
Overlord Minion Sidekick Henchman Mastermind Genius Prodigy Wizard
Boomerang Catapult Trebuchet Cannon Slingshot Jetpack Hovercraft
].freeze
EPIC_NAMES = [
"Operation Rubber Duck",
"Project Unicorn Tears",
"Mission Impossible Deadline",
"The Great Refactoring",
"Caffeine-Driven Development",
"Bug Hunt 3000",
"The Neverending Sprint",
"Deploy and Pray",
"Technical Debt Apocalypse",
"Feature Creep Chronicles",
"The Spaghetti Untangler",
"Quantum Bug Squasher",
"Legacy Code Archaelogy",
"The Memory Leak Saga",
"Operation Stack Overflow",
"Project Copy-Paste",
"The Infinite Loop Escape",
"Null Pointer Nightmare",
"The Merge Conflict Resolution",
"Friday Deploy Disaster",
"The Production Firefighter",
"Scope Creep Monster",
"The Database Migration Dance",
"Operation Hot Fix",
"The Dependency Hell Escape",
"Project Works On My Machine",
"The Meeting That Could Be An Email",
"Agile Theater Production",
"The Standup Marathon",
"Operation Ship It",
"The Code Review Gauntlet",
"Project TODO Later",
"The Callback Pyramid",
"Async Await Awakening",
"The Cache Invalidation Puzzle",
"Operation Naming Things",
"The Off-By-One Adventure",
"Project YAGNI Violation",
"The Premature Optimization",
"Rubber Band Architecture"
].freeze
SUMMARY_TEMPLATES = [
"Implement %s feature",
"Fix %s bug in module",
"Update %s documentation",
"Refactor %s component",
"Add tests for %s",
"Optimize %s performance",
"Review %s code",
"Configure %s settings",
"Deploy %s changes",
"Investigate %s issue",
"Upgrade %s dependencies",
"Add support for %s",
"Remove deprecated %s code",
"Improve %s error handling",
"Create %s endpoint",
"Design %s architecture",
"Migrate %s to new version",
"Debug %s failure",
"Enable %s functionality",
"Disable legacy %s behavior",
"Document %s API",
"Benchmark %s operations",
"Profile %s bottleneck",
"Secure %s endpoint",
"Validate %s input",
"Sanitize %s data",
"Cache %s responses",
"Index %s records",
"Batch %s processing",
"Stream %s events",
"Monitor %s health",
"Alert on %s errors",
"Audit %s access",
"Archive %s data",
"Restore %s backup",
"Scale %s infrastructure",
"Load balance %s traffic",
"Throttle %s requests",
"Rate limit %s API",
"Encrypt %s storage",
"Decrypt %s payload",
"Compress %s assets",
"Minify %s resources",
"Bundle %s modules",
"Split %s chunks",
"Lazy load %s components",
"Preload %s dependencies",
"Prefetch %s data",
"Invalidate %s cache",
"Purge %s records"
].freeze
WORDS = %w[
authentication authorization caching database email export import logging
notification pagination reporting search security settings storage sync
upload validation webhook workflow analytics dashboard metrics monitoring
backup integration migration scheduling templating versioning filtering
routing session token encryption hashing compression serialization
deserialization marshalling unmarshalling encoding decoding parsing
formatting rendering templating caching queuing messaging streaming
batching throttling limiting retrying timeout connection pooling
transaction locking concurrency threading parallelism asynchronous
synchronous blocking nonblocking callback promise future observable
subscription publishing consuming producing indexing searching
sorting filtering grouping aggregating joining merging splitting
partitioning sharding replication failover recovery snapshot
checkpoint rollback commit abort isolation consistency durability
atomicity availability partition tolerance latency throughput
bandwidth capacity utilization saturation scalability elasticity
resilience redundancy fault tolerance high availability disaster
recovery business continuity service level agreement performance
optimization tuning profiling benchmarking load testing stress
testing chaos engineering canary deployment blue green deployment
rolling update feature flag circuit breaker bulkhead retry
fallback graceful degradation health check readiness liveness
tracing logging monitoring alerting dashboarding visualization
reporting auditing compliance governance policy enforcement
access control identity management single sign on multi factor
authentication password policy credential rotation secret
management certificate management key management encryption
decryption signing verification hashing salting stretching
input validation output encoding error handling exception
management resource cleanup memory management garbage collection
reference counting object pooling connection pooling thread
configuration deployment provisioning orchestration automation
infrastructure code continuous integration delivery deployment
pipeline artifact repository container registry image scanning
vulnerability assessment penetration testing security audit
user interface experience design accessibility internationalization
localization globalization responsive adaptive progressive
enhancement graceful degradation browser compatibility cross
platform native hybrid mobile desktop embedded realtime batch
event driven message queue pub sub request response rest graphql
grpc websocket server sent events long polling short polling
slop slosh spill splatter mess sludge muck botch
webhook callback trigger scheduler cron job background worker
queue consumer producer broker exchange routing binding dead
letter retry poison acknowledgment confirmation delivery
guarantee exactly once at least once at most once idempotency
deduplication ordering sequencing windowing watermark late
arrival out of order replay reprocessing backfill migration
schema evolution backward forward compatibility versioning
deprecation sunset end of life maintenance support patching
hotfix release candidate general availability beta alpha
preview experimental stable latest edge nightly snapshot
artifact dependency resolution conflict diamond problem
circular reference lazy eager loading initialization bootstrap
startup shutdown graceful termination signal handling cleanup
resource release connection close file handle socket descriptor
memory leak resource exhaustion denial of service rate limiting
admission control backpressure flow control congestion avoidance
slow start exponential backoff jitter randomization retry storm
thundering herd cache stampede hot key hot partition skew
imbalance rebalancing redistribution consistent hashing virtual
node replica placement rack awareness zone awareness region
awareness multi region multi cloud hybrid cloud edge computing
fog computing serverless function as a service platform as a
service infrastructure backend frontend middleware gateway proxy
reverse proxy load balancer service mesh sidecar ambassador
adapter facade decorator strategy factory builder singleton
prototype flyweight composite bridge observer mediator command
state template method visitor interpreter iterator chain of
responsibility memento specification repository unit of work
data mapper active record table data gateway row data gateway
domain model transaction script service layer application
controller page controller front controller intercepting filter
context object value object entity aggregate root bounded context
ubiquitous language domain driven design event sourcing command
query responsibility segregation eventual consistency strong
consistency causal consistency read your writes monotonic reads
monotonic writes session consistency prefix consistency bounded
staleness linearizability serializability snapshot isolation
read committed read uncommitted repeatable read phantom read
dirty read lost update write skew isolation level pessimistic
optimistic concurrency control multiversion timestamp ordering
two phase locking three phase commit saga compensation rollback
forward recovery backward recovery checkpoint savepoint nested
transaction distributed transaction coordinator participant
prepare commit abort timeout recovery log write ahead logging
redo undo physiological logging logical physical incremental
differential full backup point in time recovery continuous
archiving retention policy lifecycle management tiered storage
hot warm cold archive glacier deep storage object block file
network attached direct attached storage area network software
defined storage hyperconverged infrastructure virtual machine
container pod deployment replica set stateful set daemon set
job cron job config map secret persistent volume claim storage
class ingress egress network policy service account role binding
cluster role namespace resource quota limit range horizontal
vertical pod autoscaler cluster autoscaler node affinity pod
affinity anti affinity taint toleration priority preemption
disruption budget pod security policy network security group
firewall rule access list route table subnet virtual private
cloud internet gateway nat gateway vpn direct connect peering
transit hub spoke mesh topology star ring bus tree hybrid
redundant resilient fault tolerant self healing auto scaling
auto provisioning auto configuration auto discovery service
registration health monitoring performance baseline anomaly
detection root cause analysis incident response runbook playbook
automation remediation notification escalation on call rotation
maintenance window change management release management
spaghetti yolo hackathon kludge workaround duct-tape band-aid
bikeshedding yak-shaving rubber-ducking cargo-culting cowboy-coding
copypasta stackoverflow magic incantation voodoo wizardry sorcery
gremlins demons dragons unicorns ninjas pirates zombies robots
apocalypse dumpster-fire trainwreck chaos pandemonium mayhem
shenanigans tomfoolery mischief hullabaloo brouhaha kerfuffle
bamboozle flummox discombobulate befuddle perplex mystify
thingamajig doohickey whatchamacallit gizmo widget gadget
contraption apparatus doodad thingy whatnot gubbins jiggery-pokery
hocus-pocus abracadabra mumbo-jumbo gobbledygook rigmarole
hullabaloo pandemonium bedlam brouhaha ruckus commotion fracas
hodgepodge mishmash potpourri smorgasbord gallimaufry farrago
snafu fubar tarfu caterpillar butterfly cocoon metamorphosis
boondoggle fiasco debacle catastrophe calamity disaster meltdown
implosion explosion combustion conflagration inferno blaze bonfire
phoenix resurrection rebirth reincarnation rejuvenation revival
zombie vampire werewolf ghost poltergeist specter phantom wraith
gremlin goblin troll ogre monster beast creature critter varmint
caffeine espresso latte cappuccino mocha frappuccino macchiato
pizza burrito taco nacho quesadilla enchilada fajita chimichanga
pretzel bagel croissant muffin scone biscuit waffle pancake crepe
].freeze
def initialize(url:, token:, dry_run: false)
@url = url.chomp("/")
@token = token
@dry_run = dry_run
@epic_name_field = nil # Will be discovered from API errors
@httpx = HTTPX
.plugin(:basic_auth)
.with(
headers:
{
"Accept" => "application/json",
"Content-Type" => "application/json"
}
)
.bearer_auth(token)
end
def run(num_projects:, min_issues:, max_issues:)
puts "=" * 60
puts "Jira Project Creator".bold.cyan
puts "=" * 60
puts "URL: #{@url.yellow}"
puts "Projects to create: #{num_projects.to_s.green}"
puts "Issues per project: #{min_issues}-#{max_issues} (random)"
puts "Mode: #{@dry_run ? 'DRY RUN'.yellow : 'LIVE'.red.bold}"
puts "=" * 60
puts
# Verify connection
unless @dry_run
puts "Verifying connection...".cyan
server_info = get_server_info
puts "Connected to: #{server_info['serverTitle'].green} (v#{server_info['version']})"
puts
end
# Get current user for project lead
current_user = @dry_run ? { "name" => "admin" } : get_current_user
puts "Current user: #{current_user['name'].green}"
puts
# Fetch available statuses
statuses = @dry_run ? sample_statuses : fetch_statuses
puts "Available statuses: #{statuses.pluck('name').join(', ').cyan}"
puts
# Available project templates
puts "Available project templates: #{PROJECT_TEMPLATES.length.to_s.cyan}"
puts
# Ensure project categories exist
categories = @dry_run ? PROJECT_CATEGORIES.map { |c| c.merge(id: rand(1..100).to_s) } : ensure_categories_exist
puts "Available project categories: #{categories.pluck(:name).join(', ').cyan}"
puts
# Create projects
created_projects = []
total_issues = 0
used_keys = Set.new
num_projects.times do |i|
# Generate unique random key
project_key = generate_unique_key(used_keys)
used_keys << project_key
project_name = generate_project_name
puts "\n#{"- [#{i + 1}/#{num_projects}]".cyan} Creating project"
template = PROJECT_TEMPLATES.sample
category = categories.sample
project = create_project(
key: project_key,
name: project_name,
lead: current_user["name"],
template:,
category:,
used_keys:
)
if project
created_projects << project
puts " Created project: #{project['key'].green.bold} (#{project['name'].yellow})"
puts " Template: #{template.cyan}"
puts " Category: #{category[:name].magenta}" if category
# Fetch project-specific issue types (exclude subtasks)
project_issue_types = @dry_run ? sample_issue_types : fetch_project_issue_types(project["key"])
project_types = project_issue_types
.reject { |t| t["subtask"] }
.map { |t| t["name"] }
puts " Issue types: #{project_types.join(', ').cyan}"
# Create random number of issues for this project
issue_count = rand(min_issues..max_issues)
total_issues += issue_count
puts " Creating #{issue_count.to_s.yellow} issues..."
create_issues_for_project(
project_key: project["key"],
count: issue_count,
issue_types: project_types,
statuses:
)
end
end
puts "\n#{'=' * 60}"
puts "Summary".bold.cyan
puts "=" * 60
puts "Projects created: #{created_projects.length.to_s.green.bold}"
puts "Total issues created: #{total_issues.to_s.green.bold}"
puts "=" * 60
end
private
def get_server_info
get("/rest/api/2/serverInfo")
end
def get_current_user
get("/rest/api/2/myself")
end
def fetch_statuses
get("/rest/api/2/status")
end
def fetch_issue_types
get("/rest/api/2/issuetype")
end
def fetch_project_categories
get("/rest/api/2/projectCategory")
end
def create_project_category(name:, description:)
post("/rest/api/2/projectCategory", { name:, description: })
end
def ensure_categories_exist
existing = fetch_project_categories
existing_names = existing.map { |c| c["name"] }
categories = []
PROJECT_CATEGORIES.each do |cat|
if existing_names.include?(cat[:name])
found = existing.find { |c| c["name"] == cat[:name] }
categories << { id: found["id"], name: found["name"] }
else
begin
created = create_project_category(name: cat[:name], description: cat[:description])
categories << { id: created["id"], name: created["name"] }
puts " Created category: #{cat[:name].green}"
puts ""
rescue StandardError => e
puts " ⚠ Could not create category #{cat[:name]}:".yellow + " #{e.message}"
end
end
end
categories
rescue StandardError => e
puts "⚠ Warning:".yellow + " Could not fetch categories: #{e.message}"
[]
end
def sample_statuses
[
{ "id" => "1", "name" => "To Do", "statusCategory" => { "key" => "new" } },
{ "id" => "2", "name" => "In Progress", "statusCategory" => { "key" => "indeterminate" } },
{ "id" => "3", "name" => "Done", "statusCategory" => { "key" => "done" } }
]
end
def sample_issue_types
[
{ "id" => "10001", "name" => "Task", "subtask" => false },
{ "id" => "10002", "name" => "Bug", "subtask" => false },
{ "id" => "10003", "name" => "Story", "subtask" => false },
{ "id" => "10005", "name" => "Epic", "subtask" => false },
{ "id" => "10004", "name" => "Sub-task", "subtask" => true }
]
end
def fetch_project_issue_types(project_key)
# Get issue types available for this specific project
result = get("/rest/api/2/project/#{project_key}/statuses")
result.map { |item| { "id" => item["id"], "name" => item["name"], "subtask" => item["subtask"] || false } }
rescue StandardError => e
puts "⚠ Warning:".yellow + " Could not fetch project issue types: #{e.message}"
# Fallback to global issue types
fetch_issue_types
end
def create_project(key:, name:, lead:, template:, category:, used_keys:)
if @dry_run
puts "[DRY RUN]".yellow.bold + " Would create project: #{key} (template: #{template})"
return { "key" => key, "name" => name, "id" => rand(10_000..99_999).to_s }
end
# Check if project already exists, generate new key if so
current_key = key
current_name = name
while fetch_existing_project(current_key)
puts " ⟳ Project key #{current_key.yellow} already exists, generating new key..."
current_key = generate_unique_key(used_keys)
used_keys << current_key
end
# Try to create project, retry with new name if name already exists
max_retries = 5
max_retries.times do
payload = {
key: current_key,
name: current_name,
projectTypeKey: "software",
projectTemplateKey: template,
lead:,
description: "Test project created by jira_projects.rb script at #{Time.now.utc}"
}
payload[:categoryId] = category[:id] if category&.dig(:id)
begin
result = post("/rest/api/2/project", payload)
result["key"] = current_key if result
result["name"] = current_name if result
return result
rescue StandardError => e
if e.message.include?("name already exists")
current_name = generate_project_name
puts " ⟳ Project name already exists, trying: #{current_name.yellow}"
next
end
puts "✗ Error creating project #{current_key}:".red + " #{e.message}"
return nil
end
end
puts "✗ Error: Failed to create project after #{max_retries} attempts".red
nil
end
def fetch_existing_project(key)
get("/rest/api/2/project/#{key}")
rescue StandardError
nil
end
def create_issues_for_project(project_key:, count:, issue_types:, statuses:)
print " "
count.times do |i|
issue_type = issue_types.sample || "Task"
summary = generate_summary
description = generate_description
priority = PRIORITIES.sample
epic_name = issue_type == "Epic" ? EPIC_NAMES.sample : nil
issue = create_issue(
project_key:,
issue_type:,
summary:,
description:,
priority:,
epic_name:
)
next unless issue
# Transition to random status
target_status = statuses.sample
if target_status && target_status["name"] != "To Do" && target_status["name"] != "Open"
transition_issue(issue["key"], target_status)
end
print ".".green if (i + 1) % 10 == 0
end
puts " Done!".green.bold
end
def create_issue(project_key:, issue_type:, summary:, description:, priority:, epic_name: nil)
payload = {
fields: {
project: { key: project_key },
issuetype: { name: issue_type },
summary:,
description:,
priority: { name: priority }
}
}
# Add epic name field if this is an Epic and we already know the field ID
if issue_type == "Epic" && epic_name && @epic_name_field
payload[:fields][@epic_name_field] = epic_name
end
if @dry_run
{ "key" => "#{project_key}-#{rand(1..999)}", "id" => rand(10_000..99_999).to_s }
else
begin
post("/rest/api/2/issue", payload)
rescue StandardError => e
# Check if error mentions a custom field (likely Epic Name) and extract the field ID
if issue_type == "Epic" && epic_name
field_id = extract_custom_field_id(e.message)
if field_id && field_id != @epic_name_field
@epic_name_field = field_id
puts "\n#{'★'.cyan} Discovered Epic Name field: #{field_id.green}, retrying..."
payload[:fields][field_id] = epic_name
print " "
begin
return post("/rest/api/2/issue", payload)
rescue StandardError => retry_error
puts "\n#{'✗'.red} Error creating Epic after retry: #{retry_error.message}"
return nil
end
end
end
puts "\n#{'✗'.red} Error creating issue: #{e.message}"
nil
end
end
end
def extract_custom_field_id(error_message)
# Parse error message like: {"errors" => {"customfield_10105" => "Epic Name is required."}}
match = error_message.match(/"(customfield_\d+)"/)
match ? match[1] : nil
end
def transition_issue(issue_key, target_status)
return if @dry_run
begin
# Get available transitions
transitions = get("/rest/api/2/issue/#{issue_key}/transitions")
available = transitions["transitions"] || []
# Find transition to target status
transition = available.find { |t| t.dig("to", "name") == target_status["name"] }
if transition
post("/rest/api/2/issue/#{issue_key}/transitions", { transition: { id: transition["id"] } })
end
rescue StandardError
# Silently ignore transition errors - not all statuses may be reachable
end
end
def generate_project_key
# Generate a random 3-5 letter uppercase key
length = rand(3..5)
Array.new(length) { ("A".."Z").to_a.sample }.join
end
def generate_unique_key(used_keys, max_attempts: 100)
max_attempts.times do
key = generate_project_key
return key unless used_keys.include?(key)
end
# Fallback: add random suffix
"#{generate_project_key}#{rand(10..99)}"
end
def generate_project_name
"#{PROJECT_ADJECTIVES.sample} #{PROJECT_NOUNS.sample}"
end
def generate_summary
template = SUMMARY_TEMPLATES.sample
word = WORDS.sample
format(template, word)
end
def generate_description
paragraphs = Array.new(rand(1..3)) do
sentences = Array.new(rand(2..5)) do
words = Array.new(rand(5..15)) { WORDS.sample }
"#{words.first.capitalize} #{words[1..].join(' ')}."
end
sentences.join(" ")
end
paragraphs.join("\n\n")
end
def get(path)
response = @httpx.get("#{@url}#{path}")
handle_response(response)
end
def post(path, payload)
response = @httpx.post("#{@url}#{path}", json: payload)
handle_response(response)
end
def handle_response(response)
if response.is_a?(HTTPX::ErrorResponse)
raise "Connection error: #{response.error.message}"
end
case response.status
when 200..299
return {} if response.body.to_s.empty?
response.json
when 400
error_body =
begin
response.json
rescue StandardError
response.body.to_s
end
raise "Bad request (400): #{error_body}"
when 401
raise "Unauthorized (401): Check your JIRA_TOKEN"
when 403
raise "Forbidden (403): Insufficient permissions"
when 404
raise "Not found (404): #{response.body}"
else
raise "API error (#{response.status}): #{response.body}"
end
end
end
def load_env
env_file = File.join(Dir.pwd, ".env")
if File.exist?(env_file)
File.readlines(env_file).each do |line|
line = line.strip
next if line.empty? || line.start_with?("#")
key, value = line.split("=", 2)
next unless key && value
# Remove quotes if present
value = value.gsub(/\A["']|["']\z/, "")
ENV[key] = value unless ENV[key]
end
end
end
def main
options = {
projects: 1000,
min_issues: 10,
max_issues: 100,
dry_run: false
}
OptionParser.new do |opts|
opts.banner = "Usage: #{$0} [options]"
opts.on("--projects N", Integer, "Number of projects to create (default: #{options[:projects]})") do |n|
options[:projects] = n
end
opts.on("--min-issues N", Integer, "Minimum issues per project (default: #{options[:min_issues]})") do |n|
options[:min_issues] = n
end
opts.on("--max-issues N", Integer, "Maximum issues per project (default: #{options[:max_issues]})") do |n|
options[:max_issues] = n
end
opts.on("--dry-run", "Show what would be created without making API calls") do
options[:dry_run] = true
end
opts.on("-h", "--help", "Show this help message") do
puts opts
puts
puts "Environment variables (in .env file):"
puts " JIRA_URL Jira Data Center URL (e.g., https://jira.example.com)"
puts " JIRA_TOKEN Personal Access Token for authentication"
exit
end
end.parse!
# Load environment variables from .env
load_env
url = ENV.fetch("JIRA_URL", nil)
token = ENV.fetch("JIRA_TOKEN", nil)
if url.blank?
puts "#{'✗ Error:'.red.bold} JIRA_URL environment variable is required"
puts "Set it in your .env file or export it: export JIRA_URL=https://jira.example.com"
exit 1
end
if token.blank?
puts "#{'✗ Error:'.red.bold} JIRA_TOKEN environment variable is required"
puts "Set it in your .env file or export it: export JIRA_TOKEN=your_token"
exit 1
end
creator = JiraProjectCreator.new(url:, token:, dry_run: options[:dry_run])
creator.run(
num_projects: options[:projects],
min_issues: options[:min_issues],
max_issues: options[:max_issues]
)
rescue Interrupt
puts "\n#{'Aborted by user'.yellow}"
exit 1
rescue StandardError => e
puts "✗ Error:".red.bold + " #{e.message}"
puts e.backtrace.first(5).join("\n").red if ENV["DEBUG"]
exit 1
end
main if __FILE__ == $0
# rubocop:enable Metrics/CollectionLiteralLength, Metrics/PerceivedComplexity, Metrics/AbcSize