issuer 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.vale/config/vocabularies/issuer/accept.txt +63 -0
  4. data/.vale/config/vocabularies/issuer/reject.txt +21 -0
  5. data/.vale.ini +42 -0
  6. data/Dockerfile +43 -0
  7. data/LICENSE +21 -0
  8. data/README.adoc +539 -0
  9. data/Rakefile +70 -0
  10. data/bin/console +0 -0
  11. data/bin/issuer +13 -0
  12. data/bin/setup +0 -0
  13. data/examples/README.adoc +56 -0
  14. data/examples/advanced-stub-example.yml +50 -0
  15. data/examples/basic-example.yml +33 -0
  16. data/examples/minimal-example.yml +9 -0
  17. data/examples/new-project-issues.yml +162 -0
  18. data/examples/validation-test.yml +8 -0
  19. data/exe/issuer +5 -0
  20. data/issuer.gemspec +43 -0
  21. data/lib/issuer/apis/github/client.rb +124 -0
  22. data/lib/issuer/cache.rb +197 -0
  23. data/lib/issuer/cli.rb +241 -0
  24. data/lib/issuer/issue.rb +393 -0
  25. data/lib/issuer/ops.rb +281 -0
  26. data/lib/issuer/sites/base.rb +109 -0
  27. data/lib/issuer/sites/factory.rb +31 -0
  28. data/lib/issuer/sites/github.rb +248 -0
  29. data/lib/issuer/version.rb +21 -0
  30. data/lib/issuer.rb +238 -0
  31. data/scripts/build.sh +40 -0
  32. data/scripts/lint-docs.sh +64 -0
  33. data/scripts/manage-runs.rb +175 -0
  34. data/scripts/pre-commit-template.sh +54 -0
  35. data/scripts/publish.sh +92 -0
  36. data/scripts/setup-vale.sh +59 -0
  37. data/specs/tests/README.adoc +451 -0
  38. data/specs/tests/check-github-connectivity.sh +130 -0
  39. data/specs/tests/cleanup-github-tests.sh +374 -0
  40. data/specs/tests/github-api/01-auth-connection.yml +21 -0
  41. data/specs/tests/github-api/02-basic-issues.yml +90 -0
  42. data/specs/tests/github-api/03-milestone-tests.yml +58 -0
  43. data/specs/tests/github-api/04-label-tests.yml +98 -0
  44. data/specs/tests/github-api/05-assignment-tests.yml +55 -0
  45. data/specs/tests/github-api/06-automation-tests.yml +102 -0
  46. data/specs/tests/github-api/07-error-tests.yml +29 -0
  47. data/specs/tests/github-api/08-complex-tests.yml +197 -0
  48. data/specs/tests/github-api/config.yml.example +17 -0
  49. data/specs/tests/rspec/cli_spec.rb +127 -0
  50. data/specs/tests/rspec/issue_spec.rb +184 -0
  51. data/specs/tests/rspec/issuer_spec.rb +5 -0
  52. data/specs/tests/rspec/ops_spec.rb +124 -0
  53. data/specs/tests/rspec/spec_helper.rb +54 -0
  54. data/specs/tests/run-github-api-tests.sh +424 -0
  55. metadata +200 -0
@@ -0,0 +1,50 @@
1
+ # Advanced example showing stub functionality and tag logic
2
+ $meta:
3
+ proj: docops/advanced-project
4
+ defaults:
5
+ vrsn: 2.0.0
6
+ user: team-lead
7
+ tags: [needs-review, +auto-generated] # + prefix = append to all
8
+ stub: true
9
+ head: |
10
+ ## 🤖 Auto-Generated Issue
11
+ This issue was created by the issuer CLI tool.
12
+
13
+ ---
14
+ body: |
15
+ This is a placeholder. Please update with specific requirements
16
+ and assign to the appropriate team member.
17
+ tail: |
18
+ ---
19
+
20
+ **Created by**: issuer CLI
21
+ **Project**: Advanced Features Demo
22
+
23
+ issues:
24
+ # Scalar string issues (get all defaults + stub processing)
25
+ - Implement advanced search functionality
26
+ - Add real-time notifications
27
+ - Create user dashboard
28
+
29
+ # Issue with custom content but stub enabled
30
+ - summ: Database performance optimization
31
+ stub: true
32
+ body: |
33
+ The current database queries are slow for large datasets.
34
+ Need to implement proper indexing and query optimization.
35
+ tags: [performance, database]
36
+ user: db-specialist
37
+
38
+ # Issue with stub disabled (no header/footer added)
39
+ - summ: Critical security vulnerability fix
40
+ stub: false
41
+ body: |
42
+ URGENT: Security vulnerability discovered in authentication module.
43
+ Immediate fix required.
44
+ tags: [critical, security, +urgent] # + prefix = append even with explicit tags
45
+ user: security-team
46
+ vrsn: 1.9.1
47
+
48
+ # Issue with only append tags (gets default tags too)
49
+ - summ: Code refactoring initiative
50
+ tags: [+refactoring, +technical-debt]
@@ -0,0 +1,33 @@
1
+ # Basic example demonstrating core IMYML features
2
+ $meta:
3
+ proj: myorg/myproject
4
+ defaults:
5
+ vrsn: 1.0.0
6
+ user: project-manager
7
+ tags: [enhancement]
8
+
9
+ issues:
10
+ - summ: Add user authentication
11
+ body: |
12
+ Implement secure login and registration functionality.
13
+
14
+ Requirements:
15
+ - Password hashing
16
+ - Session management
17
+ - Two-factor authentication support
18
+ tags: [security, authentication]
19
+ user: backend-dev
20
+
21
+ - summ: Fix responsive design issues
22
+ body: |
23
+ The current layout breaks on mobile devices.
24
+ Need to ensure proper responsive behavior.
25
+ tags: [bug, ui, mobile]
26
+ user: frontend-dev
27
+
28
+ - summ: Create API documentation
29
+ body: |
30
+ Generate comprehensive API documentation for developers.
31
+ Include examples and authentication details.
32
+ vrsn: 1.1.0
33
+ tags: [documentation, api]
@@ -0,0 +1,9 @@
1
+ # Minimal example for quick testing
2
+ $meta:
3
+ proj: test/repo
4
+
5
+ issues:
6
+ - summ: First test issue
7
+ body: Simple test issue for validation
8
+ - summ: Second test issue
9
+ - Quick issue from string
@@ -0,0 +1,162 @@
1
+ # New Project Bootstrap Issues
2
+ # Common tasks for setting up a new software project
3
+
4
+ $meta:
5
+ proj: myorg/new-project
6
+ defaults:
7
+ vrsn: 0.1.0
8
+ user: project-lead
9
+ tags: [project-setup, +new-project]
10
+ stub: true
11
+ head: |
12
+ ## 🚀 Project Bootstrap Task
13
+ This is a foundational task for setting up the new project.
14
+
15
+ ---
16
+ body: |
17
+ Please complete this task as part of the initial project setup.
18
+ Assign to the appropriate team member and update with specific requirements.
19
+ tail: |
20
+ ---
21
+
22
+ **Priority**: Foundation work - should be completed early
23
+ **Created by**: Project bootstrap automation
24
+
25
+ issues:
26
+ # Repository and Infrastructure Setup
27
+ - summ: Initialize repository structure
28
+ stub: false
29
+ body: |
30
+ Set up the basic repository structure and initial files.
31
+
32
+ **Tasks:**
33
+ - Create main branch protection rules
34
+ - Set up .gitignore file
35
+ - Add initial directory structure
36
+ - Configure repository settings and permissions
37
+ tags: [infrastructure, repository]
38
+ user: devops-lead
39
+
40
+ - summ: Add project README and documentation
41
+ body: |
42
+ Create comprehensive project documentation.
43
+
44
+ **Requirements:**
45
+ - Project overview and purpose
46
+ - Installation instructions
47
+ - Usage examples
48
+ - Contributing guidelines
49
+ - API documentation (if applicable)
50
+ tags: [documentation, +high-priority]
51
+ user: tech-writer
52
+
53
+ - summ: Set up CI/CD pipeline
54
+ body: |
55
+ Configure automated testing and deployment.
56
+
57
+ **Components:**
58
+ - GitHub Actions / Jenkins / CircleCI setup
59
+ - Automated testing on pull requests
60
+ - Code quality checks (linting, formatting)
61
+ - Deployment automation for staging/production
62
+ tags: [infrastructure, ci-cd, automation]
63
+ user: devops-lead
64
+
65
+ # Development Environment
66
+ - summ: Configure development environment setup
67
+ body: |
68
+ Create reproducible development environment.
69
+
70
+ **Deliverables:**
71
+ - Docker configuration or development containers
72
+ - Local setup instructions
73
+ - Environment variable documentation
74
+ - IDE/editor configuration files
75
+ tags: [development, environment]
76
+ user: senior-dev
77
+
78
+ - summ: Set up code quality tools
79
+ body: |
80
+ Implement code quality and consistency tools.
81
+
82
+ **Tools to configure:**
83
+ - Linters (ESLint, RuboCop, Pylint, etc.)
84
+ - Code formatters (Prettier, Black, etc.)
85
+ - Pre-commit hooks
86
+ - Code coverage reporting
87
+ tags: [development, code-quality]
88
+ user: senior-dev
89
+
90
+ # Testing Infrastructure
91
+ - summ: Implement testing framework
92
+ body: |
93
+ Set up comprehensive testing infrastructure.
94
+
95
+ **Test types:**
96
+ - Unit tests
97
+ - Integration tests
98
+ - End-to-end tests (if applicable)
99
+ - Performance/load tests (if needed)
100
+ tags: [testing, framework]
101
+ user: qa-lead
102
+
103
+ # Security and Compliance
104
+ - summ: Configure security scanning and policies
105
+ body: |
106
+ Implement security best practices from the start.
107
+
108
+ **Security measures:**
109
+ - Dependency vulnerability scanning
110
+ - Static code analysis for security issues
111
+ - Secrets management setup
112
+ - Security policy documentation
113
+ tags: [security, compliance, +critical]
114
+ user: security-team
115
+
116
+ # Project Management
117
+ - summ: Set up project tracking and issue management
118
+ body: |
119
+ Configure project management tools and workflows.
120
+
121
+ **Setup tasks:**
122
+ - Issue templates and labels
123
+ - Project boards/milestones
124
+ - Sprint planning tools
125
+ - Team communication channels
126
+ tags: [project-management, workflow]
127
+ user: project-manager
128
+
129
+ # Basic Implementation Tasks
130
+ - Create initial project architecture
131
+ - Implement basic routing/navigation structure
132
+ - Set up database schema and migrations
133
+ - Create user authentication system
134
+ - Implement basic API endpoints
135
+ - Add logging and monitoring setup
136
+
137
+ # Deployment and Operations
138
+ - summ: Configure staging environment
139
+ body: |
140
+ Set up staging environment for testing.
141
+
142
+ **Requirements:**
143
+ - Mirror production configuration
144
+ - Automated deployment from main branch
145
+ - Test data seeding
146
+ - Monitoring and logging
147
+ tags: [infrastructure, staging, deployment]
148
+ user: devops-lead
149
+ vrsn: 0.2.0
150
+
151
+ - summ: Prepare production deployment strategy
152
+ body: |
153
+ Plan and document production deployment.
154
+
155
+ **Planning items:**
156
+ - Infrastructure requirements
157
+ - Deployment rollback procedures
158
+ - Performance monitoring setup
159
+ - Disaster recovery plan
160
+ tags: [infrastructure, production, deployment]
161
+ user: devops-lead
162
+ vrsn: 1.0.0
@@ -0,0 +1,8 @@
1
+ issues:
2
+ - summ: Test milestone validation
3
+ desc: This issue has a milestone that probably doesn't exist
4
+ vrsn: test-milestone-v1.0
5
+ tags: [test-label-1, existing-bug]
6
+ - summ: Test label validation
7
+ desc: This issue has labels that probably don't exist
8
+ tags: [test-label-2, test-label-3, enhancement]
data/exe/issuer ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "issuer"
4
+
5
+ Issuer::CLI.start(ARGV)
data/issuer.gemspec ADDED
@@ -0,0 +1,43 @@
1
+ require_relative 'lib/issuer/version'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "issuer"
5
+ spec.version = Issuer::VERSION
6
+ spec.authors = ["DocOps Lab"]
7
+ spec.email = ["codewriter@protonmail.com"]
8
+
9
+ spec.summary = "Bulk GitHub issue creator from YAML definitions"
10
+ spec.description = "CLI tool for creating multiple GitHub issues from a single YAML file (IMYML format). Define all your issues in one place, apply defaults, and post them to GitHub in bulk."
11
+ spec.homepage = "https://github.com/DocOps/issuer"
12
+ spec.license = "MIT"
13
+ spec.required_ruby_version = ">= 2.7.0"
14
+
15
+ spec.metadata["homepage_uri"] = spec.homepage
16
+ spec.metadata["source_code_uri"] = "https://github.com/DocOps/issuer"
17
+ spec.metadata["changelog_uri"] = "https://github.com/DocOps/issuer/blob/main/CHANGELOG.md"
18
+
19
+ # Specify which files should be added to the gem when it is released.
20
+ spec.files = Dir.chdir(__dir__) do
21
+ `git ls-files -z`.split("\x0").reject do |f|
22
+ (File.expand_path(f) == __FILE__) ||
23
+ f.start_with?(*%w[test/ spec/ features/ .git .circleci appveyor Gemfile pkg/]) ||
24
+ f.match?(/\.gem$/) ||
25
+ f.match?(/test_.*\.rb$/)
26
+ end
27
+ end
28
+
29
+ spec.bindir = "exe"
30
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
31
+ spec.require_paths = ["lib"]
32
+
33
+ # Runtime dependencies
34
+ spec.add_dependency "octokit", "~> 8.0"
35
+ spec.add_dependency "thor", "~> 1.0"
36
+ spec.add_dependency "faraday-retry", "~> 2.0"
37
+
38
+ # Development dependencies
39
+ spec.add_development_dependency "bundler", "~> 2.0"
40
+ spec.add_development_dependency "rake", "~> 13.0"
41
+ spec.add_development_dependency "rspec", "~> 3.0"
42
+ spec.add_development_dependency "asciidoctor", "~> 2.0"
43
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'octokit'
4
+
5
+ module Issuer
6
+ module APIs
7
+ module GitHub
8
+ class Client
9
+ def initialize token: nil, token_env_var: nil
10
+ @token = token || detect_github_token(token_env_var)
11
+
12
+ unless @token
13
+ env_vars = [token_env_var, *default_token_env_vars].compact.uniq
14
+ raise Issuer::Error, "GitHub token not found. Set #{env_vars.join(', ')} environment variable."
15
+ end
16
+
17
+ @client = Octokit::Client.new(access_token: @token)
18
+ @client.auto_paginate = true
19
+ end
20
+
21
+ def create_issue repo, issue_params
22
+ # Validate required fields
23
+ unless issue_params[:title] && !issue_params[:title].strip.empty?
24
+ raise Issuer::Error, "Issue title is required"
25
+ end
26
+
27
+ # Prepare issue creation parameters
28
+ params = {
29
+ title: issue_params[:title],
30
+ body: issue_params[:body] || ''
31
+ }
32
+
33
+ # Handle labels
34
+ if issue_params[:labels] && !issue_params[:labels].empty?
35
+ params[:labels] = issue_params[:labels].map(&:strip).reject(&:empty?)
36
+ end
37
+
38
+ # Handle assignee
39
+ if issue_params[:assignee] && !issue_params[:assignee].strip.empty?
40
+ params[:assignee] = issue_params[:assignee].strip
41
+ end
42
+
43
+ # Handle milestone - only if milestone exists
44
+ if issue_params[:milestone]
45
+ milestone = find_milestone(repo, issue_params[:milestone])
46
+ params[:milestone] = milestone.number if milestone
47
+ end
48
+
49
+ @client.create_issue(repo, params[:title], params[:body], params)
50
+ rescue Octokit::Error => e
51
+ raise Issuer::Error, "GitHub API error: #{e.message}"
52
+ end
53
+
54
+ def find_milestone repo, milestone_title
55
+ milestones = @client.milestones(repo, state: 'all')
56
+ milestones.find { |m| m.title == milestone_title.to_s }
57
+ rescue Octokit::Error => e
58
+ raise Issuer::Error, "Error fetching milestones: #{e.message}"
59
+ end
60
+
61
+ def create_milestone repo, title, description: nil
62
+ @client.create_milestone(repo, title, description: description)
63
+ rescue Octokit::Error => e
64
+ raise Issuer::Error, "Error creating milestone '#{title}': #{e.message}"
65
+ end
66
+
67
+ def find_label repo, label_name
68
+ labels = @client.labels(repo)
69
+ labels.find { |l| l.name == label_name.to_s }
70
+ rescue Octokit::Error => e
71
+ raise Issuer::Error, "Error fetching labels: #{e.message}"
72
+ end
73
+
74
+ def create_label repo, name, color: 'f29513', description: nil
75
+ @client.add_label(repo, name, color, description: description)
76
+ rescue Octokit::Error => e
77
+ raise Issuer::Error, "Error creating label '#{name}': #{e.message}"
78
+ end
79
+
80
+ def get_milestones repo
81
+ @client.milestones(repo, state: 'all')
82
+ rescue Octokit::Error => e
83
+ raise Issuer::Error, "Error fetching milestones: #{e.message}"
84
+ end
85
+
86
+ def get_labels repo
87
+ @client.labels(repo)
88
+ rescue Octokit::Error => e
89
+ raise Issuer::Error, "Error fetching labels: #{e.message}"
90
+ end
91
+
92
+ def test_connection
93
+ @client.user
94
+ true
95
+ rescue Octokit::Error => e
96
+ raise Issuer::Error, "GitHub connection test failed: #{e.message}"
97
+ end
98
+
99
+ def rate_limit
100
+ @client.rate_limit
101
+ end
102
+
103
+ private
104
+
105
+ def default_token_env_vars
106
+ %w[ISSUER_API_TOKEN ISSUER_GITHUB_TOKEN GITHUB_ACCESS_TOKEN GITHUB_TOKEN]
107
+ end
108
+
109
+ def detect_github_token(custom_env_var)
110
+ # Check custom env var first if provided
111
+ return ENV[custom_env_var] if custom_env_var && ENV[custom_env_var]
112
+
113
+ # Fall back to standard env vars
114
+ default_token_env_vars.each do |env_var|
115
+ token = ENV[env_var]
116
+ return token if token
117
+ end
118
+
119
+ nil
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'json'
5
+ require 'digest'
6
+ require 'securerandom'
7
+ require 'time'
8
+
9
+ module Issuer
10
+ module Cache
11
+ module_function
12
+
13
+ # Directory and file management
14
+ def cache_dir
15
+ if ENV['ISSUER_CONFIG_DIR']
16
+ File.expand_path(ENV['ISSUER_CONFIG_DIR'])
17
+ elsif ENV['XDG_CONFIG_HOME']
18
+ File.join(ENV['XDG_CONFIG_HOME'], 'issuer')
19
+ else
20
+ File.expand_path('~/.config/issuer')
21
+ end
22
+ rescue ArgumentError
23
+ # Fallback if home directory issues
24
+ File.expand_path('.issuer', Dir.pwd)
25
+ end
26
+
27
+ def logs_dir
28
+ File.join(cache_dir, 'logs')
29
+ end
30
+
31
+ def ensure_cache_directories
32
+ FileUtils.mkdir_p(logs_dir) unless Dir.exist?(logs_dir)
33
+ end
34
+
35
+ def run_log_file(run_id)
36
+ File.join(logs_dir, "#{run_id}.json")
37
+ end
38
+
39
+ def generate_run_id
40
+ timestamp = Time.now.strftime('%Y%m%d_%H%M%S')
41
+ random_suffix = SecureRandom.hex(4)
42
+ "run_#{timestamp}_#{random_suffix}"
43
+ end
44
+
45
+ # Run tracking
46
+ def start_run(metadata = {})
47
+ ensure_cache_directories
48
+
49
+ run_id = generate_run_id
50
+ run_data = {
51
+ run_id: run_id,
52
+ started_at: Time.now.iso8601,
53
+ status: 'in_progress',
54
+ metadata: metadata,
55
+ artifacts: {
56
+ issues: [],
57
+ milestones: [],
58
+ labels: []
59
+ },
60
+ summary: {
61
+ issues_created: 0,
62
+ milestones_created: 0,
63
+ labels_created: 0
64
+ }
65
+ }
66
+
67
+ save_run_log(run_id, run_data)
68
+ run_id
69
+ end
70
+
71
+ def complete_run(run_id, issues_processed = nil)
72
+ run_data = load_run_log(run_id)
73
+ return unless run_data
74
+
75
+ # Handle both symbol and string keys
76
+ completed_key = run_data.key?(:completed_at) ? :completed_at : 'completed_at'
77
+ status_key = run_data.key?(:status) ? :status : 'status'
78
+
79
+ run_data[completed_key] = Time.now.iso8601
80
+ run_data[status_key] = 'completed'
81
+
82
+ # Update issues processed if provided
83
+ if issues_processed
84
+ summary_key = run_data.key?(:summary) ? :summary : 'summary'
85
+ processed_key = 'issues_processed'
86
+ run_data[summary_key][processed_key] = issues_processed
87
+ end
88
+
89
+ save_run_log(run_id, run_data)
90
+ end
91
+
92
+ def fail_run(run_id, error_message)
93
+ run_data = load_run_log(run_id)
94
+ return unless run_data
95
+
96
+ # Handle both symbol and string keys
97
+ failed_key = run_data.key?(:failed_at) ? :failed_at : 'failed_at'
98
+ status_key = run_data.key?(:status) ? :status : 'status'
99
+ error_key = run_data.key?(:error) ? :error : 'error'
100
+
101
+ run_data[failed_key] = Time.now.iso8601
102
+ run_data[status_key] = 'failed'
103
+ run_data[error_key] = error_message
104
+ save_run_log(run_id, run_data)
105
+ end
106
+
107
+ # Artifact tracking
108
+ def log_issue_created(run_id, issue_data)
109
+ log_artifact(run_id, :issues, issue_data)
110
+ end
111
+
112
+ def log_milestone_created(run_id, milestone_data)
113
+ log_artifact(run_id, :milestones, milestone_data)
114
+ end
115
+
116
+ def log_label_created(run_id, label_data)
117
+ log_artifact(run_id, :labels, label_data)
118
+ end
119
+
120
+ def log_artifact(run_id, type, artifact_data)
121
+ run_data = load_run_log(run_id)
122
+ return unless run_data
123
+
124
+ # Handle both symbol and string keys (since JSON loading converts symbols to strings)
125
+ artifacts_key = run_data.key?(:artifacts) ? :artifacts : 'artifacts'
126
+ summary_key = run_data.key?(:summary) ? :summary : 'summary'
127
+ type_key = run_data[artifacts_key].key?(type) ? type : type.to_s
128
+
129
+ run_data[artifacts_key][type_key] << artifact_data
130
+
131
+ # Increment the appropriate counter (use proper plural-to-singular mapping)
132
+ counter_key = case type.to_s
133
+ when 'issues' then 'issues_created'
134
+ when 'milestones' then 'milestones_created'
135
+ when 'labels' then 'labels_created'
136
+ else "#{type}_created"
137
+ end
138
+
139
+ summary_section = run_data[summary_key]
140
+ if summary_section.key?(counter_key.to_sym)
141
+ summary_section[counter_key.to_sym] += 1
142
+ elsif summary_section.key?(counter_key)
143
+ summary_section[counter_key] += 1
144
+ else
145
+ # Fallback - create the key as string
146
+ summary_section[counter_key] = 1
147
+ end
148
+
149
+ save_run_log(run_id, run_data)
150
+ end
151
+
152
+ # Data persistence
153
+ def save_run_log(run_id, data)
154
+ File.write(run_log_file(run_id), JSON.pretty_generate(data))
155
+ end
156
+
157
+ def load_run_log(run_id)
158
+ log_file = run_log_file(run_id)
159
+ return nil unless File.exist?(log_file)
160
+
161
+ JSON.parse(File.read(log_file), symbolize_names: true)
162
+ rescue JSON::ParserError => e
163
+ puts "⚠️ Warning: Could not parse run log file #{log_file}: #{e.message}"
164
+ nil
165
+ end
166
+
167
+ # Query and listing
168
+ def list_runs(status: nil, limit: nil)
169
+ ensure_cache_directories
170
+
171
+ log_files = Dir.glob(File.join(logs_dir, '*.json'))
172
+ .sort_by { |f| File.mtime(f) }
173
+ .reverse
174
+
175
+ runs = log_files.map do |file|
176
+ begin
177
+ data = JSON.parse(File.read(file), symbolize_names: true)
178
+ next if status && data[:status] != status.to_s
179
+ data
180
+ rescue JSON::ParserError
181
+ nil
182
+ end
183
+ end.compact
184
+
185
+ limit ? runs.take(limit) : runs
186
+ end
187
+
188
+ def get_run(run_id)
189
+ load_run_log(run_id)
190
+ end
191
+
192
+ def delete_run_log(run_id)
193
+ log_file = run_log_file(run_id)
194
+ File.delete(log_file) if File.exist?(log_file)
195
+ end
196
+ end
197
+ end