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
data/lib/issuer.rb ADDED
@@ -0,0 +1,238 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "issuer/version"
4
+ require_relative "issuer/issue"
5
+ require_relative "issuer/apis/github/client"
6
+ require_relative "issuer/sites/base"
7
+ require_relative "issuer/sites/github"
8
+ require_relative "issuer/sites/factory"
9
+ require_relative "issuer/ops"
10
+ require_relative "issuer/cache"
11
+ require_relative "issuer/cli"
12
+
13
+ ##
14
+ # = Issuer: Bulk GitHub Issue Creator
15
+ #
16
+ # Issuer is a Ruby gem that allows you to define all your work tickets in one place
17
+ # using the IMYML (Issue Management YAML-based Modeling Language) format, apply defaults,
18
+ # and post them to GitHub Issues (and other platforms) in bulk.
19
+ #
20
+ # == Features
21
+ #
22
+ # * Bulk issue creation from a single YAML file
23
+ # * Dry-run mode for testing without creating issues
24
+ # * Automatic milestone and label creation
25
+ # * Configurable defaults and label application logic
26
+ # * Environment variable support for authentication
27
+ # * Issue validation with helpful error messages
28
+ # * Extensible architecture for multiple platforms (GitHub, JIRA, GitLab, etc.)
29
+ # * Run logging and artifact tracking for cleanup operations
30
+ #
31
+ # == Quick Start
32
+ #
33
+ # # Create issues from an IMYML file
34
+ # processor = Issuer::Processor.new
35
+ # results = processor.process_file('issues.yml',
36
+ # proj: 'myorg/myrepo',
37
+ # dry_run: false)
38
+ #
39
+ # # Access individual components
40
+ # site = Issuer::Sites::Factory.create(:github)
41
+ # issues = Issuer::Issue.from_array(yaml_data['issues'], defaults)
42
+ #
43
+ # == Extensibility
44
+ #
45
+ # The gem is designed for extensibility:
46
+ #
47
+ # * Create new site adapters by subclassing {Issuer::Sites::Base}
48
+ # * Extend the IMYML format by customizing {Issuer::Ops} methods
49
+ # * Add custom validation logic to {Issuer::Issue}
50
+ # * Track operations with {Issuer::Cache}
51
+ #
52
+ # @see https://github.com/DocOps/issuer Project Homepage
53
+ # @see Issuer::Sites::Base For creating new platform adapters
54
+ # @see Issuer::Issue For the core issue model
55
+ # @see Issuer::Ops For IMYML processing operations
56
+ #
57
+ module Issuer
58
+
59
+ ##
60
+ # Standard error class for all Issuer-related errors
61
+ class Error < StandardError; end
62
+
63
+ ##
64
+ # Main processor class that provides a clean API for external usage.
65
+ # This is the recommended entry point for programmatic access to Issuer functionality.
66
+ #
67
+ # @example Basic usage
68
+ # processor = Issuer::Processor.new
69
+ # results = processor.process_file('issues.yml', proj: 'myorg/myrepo')
70
+ #
71
+ # @example With custom site
72
+ # site = Issuer::Sites::Factory.create(:github, token: 'custom-token')
73
+ # processor = Issuer::Processor.new(site: site)
74
+ # results = processor.process_data(yaml_data, proj: 'myorg/myrepo')
75
+ #
76
+ class Processor
77
+
78
+ ##
79
+ # Initialize a new processor
80
+ #
81
+ # @param site [Issuer::Sites::Base, nil] Custom site adapter. If nil, will auto-detect.
82
+ # @param cache [Boolean] Whether to enable run tracking and caching (default: true)
83
+ #
84
+ def initialize(site: nil, cache: true)
85
+ @site = site
86
+ @cache_enabled = cache
87
+ end
88
+
89
+ ##
90
+ # Process an IMYML file and create issues
91
+ #
92
+ # @param file_path [String] Path to the IMYML YAML file
93
+ # @param proj [String, nil] Target repository (org/repo format)
94
+ # @param dry_run [Boolean] If true, validate and show what would be created without API calls
95
+ # @param automation_options [Hash] Options for automatic resource creation
96
+ # @option automation_options [Boolean] :auto_versions Automatically create missing milestones
97
+ # @option automation_options [Boolean] :auto_tags Automatically create missing labels
98
+ # @option automation_options [Boolean] :auto_metadata Automatically create all missing metadata
99
+ #
100
+ # @return [Hash] Results including created issues, milestones, labels, and run metadata
101
+ # @raise [Issuer::Error] If file cannot be read or processed
102
+ #
103
+ # @example
104
+ # results = processor.process_file('issues.yml',
105
+ # proj: 'myorg/myrepo',
106
+ # dry_run: false,
107
+ # automation_options: { auto_metadata: true })
108
+ # puts "Created #{results[:issues_created]} issues"
109
+ #
110
+ def process_file(file_path, proj: nil, dry_run: false, automation_options: {})
111
+ require 'yaml'
112
+
113
+ unless File.exist?(file_path)
114
+ raise Error, "File not found: #{file_path}"
115
+ end
116
+
117
+ begin
118
+ raw_data = YAML.load_file(file_path)
119
+ rescue => e
120
+ raise Error, "Could not parse YAML file: #{file_path}\n#{e.message}"
121
+ end
122
+
123
+ process_data(raw_data, proj: proj, dry_run: dry_run, automation_options: automation_options)
124
+ end
125
+
126
+ ##
127
+ # Process IMYML data structure and create issues
128
+ #
129
+ # @param data [Hash] Parsed IMYML data structure
130
+ # @param proj [String, nil] Target repository (org/repo format)
131
+ # @param dry_run [Boolean] If true, validate and show what would be created without API calls
132
+ # @param automation_options [Hash] Options for automatic resource creation
133
+ #
134
+ # @return [Hash] Results including created issues, milestones, labels, and run metadata
135
+ # @raise [Issuer::Error] If data is invalid or processing fails
136
+ #
137
+ def process_data(data, proj: nil, dry_run: false, automation_options: {})
138
+ # Extract metadata and issues
139
+ meta = data['$meta'] || {}
140
+ issues_data = data['issues'] || data
141
+
142
+ unless issues_data.is_a?(Array)
143
+ raise Error, 'No issues array found (root or under "issues")'
144
+ end
145
+
146
+ # Build defaults
147
+ defaults = (meta['defaults'] || {}).dup
148
+ defaults['proj'] = meta['proj'] if meta['proj']
149
+
150
+ # Determine target repository
151
+ target_repo = proj || meta['proj'] || ENV['ISSUER_REPO']
152
+ if target_repo.nil? && !dry_run
153
+ raise Error, 'No target repo specified. Use proj parameter, $meta.proj, or ISSUER_REPO environment variable.'
154
+ end
155
+
156
+ # Process issues
157
+ issues = Ops.process_issues_data(issues_data, defaults)
158
+ valid_issues = issues.select(&:valid?)
159
+ invalid_issues = issues.reject(&:valid?)
160
+
161
+ # Report validation errors
162
+ invalid_issues.each_with_index do |issue, idx|
163
+ puts "âš ī¸ Skipping invalid issue: #{issue.validation_errors.join(', ')}"
164
+ end
165
+
166
+ if dry_run
167
+ return perform_dry_run(valid_issues, target_repo)
168
+ else
169
+ return perform_live_run(valid_issues, target_repo, automation_options)
170
+ end
171
+ end
172
+
173
+ private
174
+
175
+ def get_site
176
+ @site ||= Sites::Factory.create(Sites::Factory.default_site)
177
+ end
178
+
179
+ def perform_dry_run(issues, repo)
180
+ site = Sites::Factory.create('github', token: 'dry-run-token')
181
+
182
+ puts "đŸ§Ē DRY RUN - No issues will be created"
183
+ puts "📋 Target repository: #{repo}" if repo
184
+ puts "📝 Would create #{issues.length} issues:"
185
+ puts
186
+
187
+ issues.each_with_index do |issue, idx|
188
+ params = site.convert_issue_to_site_params(issue, repo, dry_run: true)
189
+ puts "#{idx + 1}. #{params[:title]}"
190
+ puts " Labels: #{params[:labels].join(', ')}" if params[:labels]&.any?
191
+ puts " Assignee: #{params[:assignee]}" if params[:assignee]
192
+ puts " Milestone: #{params[:milestone]}" if params[:milestone]
193
+ puts
194
+ end
195
+
196
+ {
197
+ dry_run: true,
198
+ issues_planned: issues.length,
199
+ target_repo: repo,
200
+ valid_issues: issues.length
201
+ }
202
+ end
203
+
204
+ def perform_live_run(issues, repo, automation_options)
205
+ site = get_site
206
+
207
+ # Start run tracking if caching enabled
208
+ run_id = if @cache_enabled
209
+ Cache.start_run(issues_planned: issues.length, target_repo: repo)
210
+ else
211
+ nil
212
+ end
213
+
214
+ begin
215
+ # Validate and prepare resources (milestones, labels)
216
+ Ops.validate_and_prepare_resources(site, repo, issues, automation_options, run_id) unless issues.empty?
217
+
218
+ # Create issues
219
+ processed_count = site.post_issues(repo, issues, run_id)
220
+
221
+ # Complete run tracking
222
+ Cache.complete_run(run_id, processed_count) if run_id
223
+
224
+ {
225
+ dry_run: false,
226
+ issues_created: processed_count,
227
+ issues_planned: issues.length,
228
+ target_repo: repo,
229
+ run_id: run_id
230
+ }
231
+ rescue => e
232
+ # Mark run as failed
233
+ Cache.fail_run(run_id, e.message) if run_id
234
+ raise
235
+ end
236
+ end
237
+ end
238
+ end
data/scripts/build.sh ADDED
@@ -0,0 +1,40 @@
1
+ #!/bin/bash
2
+ # Release automation script for Ruby gem projects
3
+
4
+ set -e
5
+
6
+ # Load common build functions
7
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8
+ source "$SCRIPT_DIR/lib/build-common.sh"
9
+
10
+ # Project-specific configuration
11
+ PROJECT_NAME="issuer"
12
+ DOCKER_ORG="docopslab"
13
+
14
+ echo -e "${GREEN}🚀 ${PROJECT_NAME} Release Build Script${NC}"
15
+ echo "=================================="
16
+
17
+ # Validation
18
+ check_project_root
19
+ check_git_clean
20
+ check_main_branch
21
+ check_bundle_installed
22
+
23
+ # Run tests
24
+ run_rspec_tests
25
+ test_cli_functionality
26
+
27
+ # Get current version
28
+ current_version=$(get_current_version)
29
+ echo -e "${GREEN}📋 Current version: $current_version${NC}"
30
+
31
+ # Build and test gem
32
+ build_gem
33
+ gem_file=$(test_built_gem)
34
+
35
+ # Build Docker image using the docker-specific script
36
+ echo -e "${YELLOW}đŸŗ Building Docker image...${NC}"
37
+ "$SCRIPT_DIR/build-docker.sh" 2>&1 | grep -E "(Building|Testing|successfully|Docker image:)" || true
38
+
39
+ # Show final success message
40
+ show_build_success "$current_version" "$gem_file"
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env bash
2
+ # Vale linting script for the issuer project
3
+
4
+ set -e
5
+
6
+ echo "🔍 Running Vale linter on issuer project..."
7
+
8
+ # Colors for output
9
+ RED='\033[0;31m'
10
+ GREEN='\033[0;32m'
11
+ YELLOW='\033[1;33m'
12
+ NC='\033[0m' # No Color
13
+
14
+ # Check if Vale is installed
15
+ if ! command -v vale &> /dev/null; then
16
+ echo -e "${RED}❌ Vale is not installed. Please install Vale first.${NC}"
17
+ echo " You can install it from: https://github.com/errata-ai/vale"
18
+ exit 1
19
+ fi
20
+
21
+ # Check if .vale.ini exists
22
+ if [[ ! -f ".vale.ini" ]]; then
23
+ echo -e "${RED}❌ .vale.ini not found in current directory${NC}"
24
+ echo " Please run this script from the project root."
25
+ exit 1
26
+ fi
27
+
28
+ # Default to checking all relevant files if no arguments provided
29
+ if [[ $# -eq 0 ]]; then
30
+ FILES=(
31
+ "README.adoc"
32
+ "lib/examples/*.md"
33
+ "specs/tests/*.md"
34
+ "*.adoc"
35
+ "docs/*.adoc"
36
+ )
37
+ else
38
+ FILES=("$@")
39
+ fi
40
+
41
+ echo "📁 Files to check: ${FILES[*]}"
42
+ echo ""
43
+
44
+ # Run Vale
45
+ EXIT_CODE=0
46
+ for pattern in "${FILES[@]}"; do
47
+ # Use find to expand patterns and handle missing files gracefully
48
+ while IFS= read -r -d '' file; do
49
+ if [[ -f "$file" ]]; then
50
+ echo -e "${YELLOW}📄 Checking: $file${NC}"
51
+ if ! vale "$file"; then
52
+ EXIT_CODE=1
53
+ fi
54
+ fi
55
+ done < <(find . -name "$pattern" -print0 2>/dev/null)
56
+ done
57
+
58
+ if [[ $EXIT_CODE -eq 0 ]]; then
59
+ echo -e "${GREEN}✅ Vale linting completed successfully!${NC}"
60
+ else
61
+ echo -e "${RED}❌ Vale found issues. Please review and fix them.${NC}"
62
+ fi
63
+
64
+ exit $EXIT_CODE
@@ -0,0 +1,175 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Run management utility for issuer CLI
5
+ # Lists and manages cached runs in .issuer/logs/
6
+
7
+ require 'bundler/setup'
8
+ $LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
9
+ require 'issuer/cache'
10
+
11
+ def show_help
12
+ puts <<~HELP
13
+ Issuer Run Management Utility
14
+
15
+ Usage:
16
+ ruby scripts/manage-runs.rb [command]
17
+
18
+ Commands:
19
+ list List all cached runs
20
+ list --recent List recent runs only (last 10)
21
+ show RUN_ID Show detailed information for a specific run
22
+ clean-logs Remove all log files (use with caution)
23
+
24
+ Examples:
25
+ ruby scripts/manage-runs.rb list
26
+ ruby scripts/manage-runs.rb show run_20250711_180124_97d1f1f3
27
+ ruby scripts/manage-runs.rb list --recent
28
+ HELP
29
+ end
30
+
31
+ def list_runs recent_only = false
32
+ runs = Issuer::Cache.list_runs
33
+ runs = runs.take(10) if recent_only
34
+
35
+ if runs.empty?
36
+ puts "No cached runs found."
37
+ return
38
+ end
39
+
40
+ puts "Cached Runs#{recent_only ? ' (Recent)' : ''}:"
41
+ puts "=" * 60
42
+
43
+ runs.each do |run|
44
+ puts "#{run[:run_id]} - #{run[:status].upcase}"
45
+ puts " Started: #{run[:started_at]}"
46
+
47
+ if run[:metadata] && run[:metadata][:issues_planned]
48
+ puts " Issues planned: #{run[:metadata][:issues_planned]}"
49
+ end
50
+
51
+ if run[:status] == 'completed'
52
+ puts " Completed: #{run[:completed_at]}"
53
+ puts " Artifacts: #{run[:summary][:issues_created]} issues, #{run[:summary][:milestones_created]} milestones, #{run[:summary][:labels_created]} labels"
54
+ puts " Processed: #{run[:summary][:issues_processed]} issues"
55
+ elsif run[:status] == 'failed'
56
+ puts " Failed: #{run[:failed_at] || 'unknown time'}"
57
+ puts " Error: #{run[:error] || 'no error message'}"
58
+ end
59
+ puts ""
60
+ end
61
+
62
+ puts "Total: #{runs.length} runs"
63
+ puts "Use 'ruby scripts/manage-runs.rb show RUN_ID' for detailed view"
64
+ end
65
+
66
+ def show_run run_id
67
+ run_data = Issuer::Cache.get_run(run_id)
68
+ unless run_data
69
+ puts "Error: Run #{run_id} not found."
70
+ return
71
+ end
72
+
73
+ puts "Run Details: #{run_id}"
74
+ puts "=" * 60
75
+ puts "Status: #{run_data[:status]}"
76
+ puts "Started: #{run_data[:started_at]}"
77
+
78
+ if run_data[:completed_at]
79
+ puts "Completed: #{run_data[:completed_at]}"
80
+ end
81
+
82
+ if run_data[:failed_at]
83
+ puts "Failed: #{run_data[:failed_at]}"
84
+ puts "Error: #{run_data[:error]}" if run_data[:error]
85
+ end
86
+
87
+ if run_data[:metadata]
88
+ puts ""
89
+ puts "Metadata:"
90
+ run_data[:metadata].each do |key, value|
91
+ puts " #{key}: #{value}"
92
+ end
93
+ end
94
+
95
+ if run_data[:artifacts]
96
+ puts ""
97
+ puts "Artifacts Created:"
98
+
99
+ [:issues, :milestones, :labels].each do |type|
100
+ artifacts = run_data[:artifacts][type.to_s] || run_data[:artifacts][type] || []
101
+ next if artifacts.empty?
102
+
103
+ puts " #{type.to_s.capitalize} (#{artifacts.length}):"
104
+ artifacts.each do |artifact|
105
+ case type
106
+ when :issues
107
+ puts " ##{artifact[:number] || artifact['number']} - #{artifact[:title] || artifact['title']}"
108
+ puts " #{artifact[:url] || artifact['url']}"
109
+ when :milestones
110
+ puts " ##{artifact[:number] || artifact['number']} - #{artifact[:title] || artifact['title']}"
111
+ puts " #{artifact[:url] || artifact['url']}"
112
+ when :labels
113
+ puts " #{artifact[:name] || artifact['name']} (#{artifact[:color] || artifact['color']})"
114
+ puts " #{artifact[:url] || artifact['url']}"
115
+ end
116
+ puts ""
117
+ end
118
+ end
119
+ end
120
+
121
+ if run_data[:summary]
122
+ puts "Summary:"
123
+ run_data[:summary].each do |key, value|
124
+ puts " #{key}: #{value}"
125
+ end
126
+ end
127
+ end
128
+
129
+ def clean_logs
130
+ logs_dir = Issuer::Cache.logs_dir
131
+ unless Dir.exist?(logs_dir)
132
+ puts "No logs directory found."
133
+ return
134
+ end
135
+
136
+ log_files = Dir.glob(File.join(logs_dir, '*.json'))
137
+ if log_files.empty?
138
+ puts "No log files found."
139
+ return
140
+ end
141
+
142
+ puts "Found #{log_files.length} log files."
143
+ print "Are you sure you want to delete all log files? [y/N]: "
144
+ response = STDIN.gets.chomp.downcase
145
+
146
+ unless ['y', 'yes'].include?(response)
147
+ puts "Cancelled."
148
+ return
149
+ end
150
+
151
+ log_files.each { |f| File.delete(f) }
152
+ puts "Deleted #{log_files.length} log files."
153
+ end
154
+
155
+ # Main execution
156
+ case ARGV[0]
157
+ when 'list'
158
+ recent_only = ARGV.include?('--recent')
159
+ list_runs(recent_only)
160
+ when 'show'
161
+ if ARGV[1]
162
+ show_run(ARGV[1])
163
+ else
164
+ puts "Error: Please specify a run ID"
165
+ puts "Usage: ruby scripts/manage-runs.rb show RUN_ID"
166
+ end
167
+ when 'clean-logs'
168
+ clean_logs
169
+ when nil, '--help', '-h'
170
+ show_help
171
+ else
172
+ puts "Error: Unknown command '#{ARGV[0]}'"
173
+ puts ""
174
+ show_help
175
+ end
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env bash
2
+ # Pre-commit hook for Vale linting
3
+
4
+ set -e
5
+
6
+ echo "🔍 Running Vale pre-commit check..."
7
+
8
+ # Check if Vale is installed
9
+ if ! command -v vale &> /dev/null; then
10
+ echo "âš ī¸ Vale not found. Skipping documentation linting."
11
+ echo " Install Vale to enable documentation quality checks."
12
+ exit 0
13
+ fi
14
+
15
+ # Get list of staged files that we care about (excluding copilot instructions)
16
+ STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(md|adoc|asciidoc)$' | grep -v '^\.github/copilot-instructions\.md$' || true)
17
+
18
+ if [[ -z "$STAGED_FILES" ]]; then
19
+ echo "â„šī¸ No documentation files staged for commit."
20
+ exit 0
21
+ fi
22
+
23
+ echo "📄 Checking staged documentation files:"
24
+ echo "$STAGED_FILES"
25
+
26
+ # Run Vale on staged files
27
+ TEMP_DIR=$(mktemp -d)
28
+ EXIT_CODE=0
29
+
30
+ for file in $STAGED_FILES; do
31
+ if [[ -f "$file" ]]; then
32
+ # Copy staged version to temp location
33
+ git show :"$file" > "$TEMP_DIR/$(basename "$file")"
34
+
35
+ echo "🔍 Linting: $file"
36
+ if ! vale "$TEMP_DIR/$(basename "$file")"; then
37
+ EXIT_CODE=1
38
+ fi
39
+ fi
40
+ done
41
+
42
+ # Cleanup
43
+ rm -rf "$TEMP_DIR"
44
+
45
+ if [[ $EXIT_CODE -ne 0 ]]; then
46
+ echo ""
47
+ echo "❌ Vale found documentation issues in staged files."
48
+ echo " Please fix the issues above before committing."
49
+ echo " Or use 'git commit --no-verify' to bypass this check."
50
+ exit 1
51
+ fi
52
+
53
+ echo "✅ Documentation quality check passed!"
54
+ exit 0
@@ -0,0 +1,92 @@
1
+ #!/bin/bash
2
+ # Manual publishing script for Ruby gem projects
3
+
4
+ set -e
5
+
6
+ # Parse command line arguments
7
+ DRY_RUN=false
8
+ while [[ $# -gt 0 ]]; do
9
+ case $1 in
10
+ --dry-run)
11
+ DRY_RUN=true
12
+ shift
13
+ ;;
14
+ *)
15
+ echo "Unknown option: $1"
16
+ echo "Usage: $0 [--dry-run]"
17
+ exit 1
18
+ ;;
19
+ esac
20
+ done
21
+
22
+ # Load common build functions
23
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
24
+ source "$SCRIPT_DIR/lib/build-common.sh"
25
+
26
+ # Project-specific configuration
27
+ PROJECT_NAME="issuer"
28
+ DOCKER_ORG="docopslab"
29
+
30
+ echo -e "${GREEN}đŸ“Ļ ${PROJECT_NAME} Publishing Script${NC}"
31
+ echo "=============================="
32
+
33
+ # Check for required environment variables (skip in dry-run mode)
34
+ if [ "$DRY_RUN" = false ]; then
35
+ if [ -z "$RUBYGEMS_API_KEY" ]; then
36
+ echo -e "${RED}❌ Error: RUBYGEMS_API_KEY environment variable not set${NC}"
37
+ echo "Get your API key from https://rubygems.org/profile/edit"
38
+ echo "Then run: export RUBYGEMS_API_KEY=your_key_here"
39
+ exit 1
40
+ fi
41
+
42
+ if [ -z "$DOCKERHUB_USERNAME" ] || [ -z "$DOCKERHUB_TOKEN" ]; then
43
+ echo -e "${RED}❌ Error: Docker Hub credentials not set${NC}"
44
+ echo "Set DOCKERHUB_USERNAME and DOCKERHUB_TOKEN environment variables"
45
+ exit 1
46
+ fi
47
+ fi
48
+
49
+ # Get current version
50
+ current_version=$(get_current_version)
51
+ gem_file="pkg/${PROJECT_NAME}-$current_version.gem"
52
+
53
+ # Check if gem file exists
54
+ if [ ! -f "$gem_file" ]; then
55
+ echo -e "${RED}❌ Error: Gem file $gem_file not found${NC}"
56
+ echo "Run ./scripts/build.sh first"
57
+ exit 1
58
+ fi
59
+
60
+ if [ "$DRY_RUN" = true ]; then
61
+ echo -e "${YELLOW}🔍 Dry run mode enabled. The following actions would be performed:${NC}"
62
+ echo "- Publish to RubyGems: gem push $gem_file"
63
+ echo "- Login to Docker Hub and push Docker images"
64
+ else
65
+ # Publish to RubyGems
66
+ echo -e "${YELLOW}💎 Publishing to RubyGems...${NC}"
67
+ mkdir -p ~/.gem
68
+ echo ":rubygems_api_key: $RUBYGEMS_API_KEY" > ~/.gem/credentials
69
+ chmod 0600 ~/.gem/credentials
70
+ gem push "$gem_file"
71
+ echo -e "${GREEN}✅ Published to RubyGems successfully${NC}"
72
+
73
+ # Login to Docker Hub
74
+ echo -e "${YELLOW}đŸŗ Logging in to Docker Hub...${NC}"
75
+ echo "$DOCKERHUB_TOKEN" | docker login -u "$DOCKERHUB_USERNAME" --password-stdin
76
+
77
+ # Push Docker images
78
+ echo -e "${YELLOW}đŸŗ Pushing Docker images...${NC}"
79
+ docker push ${DOCKER_ORG}/${PROJECT_NAME}:$current_version
80
+ docker push ${DOCKER_ORG}/${PROJECT_NAME}:latest
81
+ echo -e "${GREEN}✅ Pushed Docker images successfully${NC}"
82
+ fi
83
+
84
+ echo
85
+ echo -e "${GREEN}🎉 Publishing completed successfully!${NC}"
86
+ echo "=============================="
87
+ echo -e "Gem: ${YELLOW}https://rubygems.org/gems/${PROJECT_NAME}/versions/$current_version${NC}"
88
+ echo -e "Docker: ${YELLOW}https://hub.docker.com/r/${DOCKER_ORG}/${PROJECT_NAME}${NC}"
89
+ echo
90
+ echo "Verify installation:"
91
+ echo " gem install ${PROJECT_NAME}"
92
+ echo " docker pull ${DOCKER_ORG}/${PROJECT_NAME}:$current_version"