github-daily-digest 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.
- checksums.yaml +7 -0
- data/README.md +35 -0
- data/Rakefile +4 -0
- data/bin/console +11 -0
- data/bin/github-daily-digest +140 -0
- data/bin/setup +8 -0
- data/github-daily-digest.gemspec +47 -0
- data/github-daily-digest.rb +20 -0
- data/lib/activity_analyzer.rb +48 -0
- data/lib/configuration.rb +260 -0
- data/lib/daily_digest_runner.rb +932 -0
- data/lib/gemini_service.rb +616 -0
- data/lib/github-daily-digest/version.rb +5 -0
- data/lib/github_daily_digest.rb +16 -0
- data/lib/github_graphql_service.rb +1191 -0
- data/lib/github_service.rb +364 -0
- data/lib/html_formatter.rb +1297 -0
- data/lib/language_analyzer.rb +163 -0
- data/lib/markdown_formatter.rb +137 -0
- data/lib/output_formatter.rb +818 -0
- metadata +178 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 5f607d1a7a04aa2735c14f7d6872c348efa10aa74c7a91296a35fd39e6705be8
|
4
|
+
data.tar.gz: b4d1d4e1de8ede127a6844919736afb6f5805ea29b6a8e2606bba3371c18bb92
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: cf2f46d7f565b40c1731e1386a6eaa394947be98168befd7f59dc805ed0f7864bf01d34bb7e38be47fdb9b99f7a8b6e174fbd119eb768540c4ec1072b3105608
|
7
|
+
data.tar.gz: 2ee9c02d0ccab93af5870ecee1525fdd4140f1b9723a0bfd231f3af4e18117db77477553062b17ad281debfbcbdee6fd227785d47dd5735f9c27da4b1dfe3aeb
|
data/README.md
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# GithubDailyDigest
|
2
|
+
|
3
|
+
TODO: Delete this and the text below, and describe your gem
|
4
|
+
|
5
|
+
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/github_daily_digest`. To experiment with that code, run `bin/console` for an interactive prompt.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
|
10
|
+
|
11
|
+
Install the gem and add to the application's Gemfile by executing:
|
12
|
+
|
13
|
+
```bash
|
14
|
+
bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
|
15
|
+
```
|
16
|
+
|
17
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
18
|
+
|
19
|
+
```bash
|
20
|
+
gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
|
21
|
+
```
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
TODO: Write usage instructions here
|
26
|
+
|
27
|
+
## Development
|
28
|
+
|
29
|
+
After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
30
|
+
|
31
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
32
|
+
|
33
|
+
## Contributing
|
34
|
+
|
35
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/github_daily_digest.
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup"
|
5
|
+
require "github-daily-digest"
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
require "irb"
|
11
|
+
IRB.start(__FILE__)
|
@@ -0,0 +1,140 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# This script uses the github_daily_digest gem to run the daily analysis.
|
4
|
+
|
5
|
+
# --- Optional: Load .env for convenience if dotenv gem is available ---
|
6
|
+
# This allows users to manage secrets via .env when running the executable,
|
7
|
+
# but the core gem library doesn't depend on it.
|
8
|
+
begin
|
9
|
+
require 'dotenv/load'
|
10
|
+
# Don't print this in json-only mode
|
11
|
+
rescue LoadError
|
12
|
+
# dotenv gem not found, proceed using environment variables set externally.
|
13
|
+
end
|
14
|
+
# --------------------------------------------------------------------
|
15
|
+
|
16
|
+
# Add the lib directory to the load path for running from the repository
|
17
|
+
$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
|
18
|
+
|
19
|
+
require 'github-daily-digest' # Load the gem's code from lib/
|
20
|
+
require 'logger'
|
21
|
+
require 'json'
|
22
|
+
require 'fileutils'
|
23
|
+
|
24
|
+
begin
|
25
|
+
# Load Configuration with command-line arguments
|
26
|
+
config = GithubDailyDigest::Configuration.new(ARGV)
|
27
|
+
|
28
|
+
# Show help and exit if requested
|
29
|
+
if config.help_requested
|
30
|
+
puts config.help_text
|
31
|
+
exit(0)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Setup Logger based on configuration
|
35
|
+
logger = if config.output_to_stdout
|
36
|
+
# Use STDOUT for direct output
|
37
|
+
logger = Logger.new(STDOUT)
|
38
|
+
# Format logs without timestamps when outputting to terminal
|
39
|
+
logger.formatter = proc do |severity, datetime, progname, msg|
|
40
|
+
config.json_only ? "" : "[#{severity.ljust(5)}]: #{msg}\n"
|
41
|
+
end
|
42
|
+
logger
|
43
|
+
else
|
44
|
+
# Traditional log file if explicitly requested
|
45
|
+
log_file_path = File.expand_path('daily_digest.log', Dir.pwd)
|
46
|
+
logger = Logger.new(log_file_path, 'daily', 7)
|
47
|
+
logger.formatter = proc do |severity, datetime, progname, msg|
|
48
|
+
"#{datetime.strftime('%Y-%m-%d %H:%M:%S')} [#{severity.ljust(5)}]: #{msg}\n"
|
49
|
+
end
|
50
|
+
puts "Logging to: #{log_file_path}"
|
51
|
+
logger
|
52
|
+
end
|
53
|
+
|
54
|
+
logger.level = Logger.const_get(config.log_level)
|
55
|
+
|
56
|
+
# Only output this in verbose mode
|
57
|
+
logger.info("Starting GitHub Daily Digest") unless config.json_only
|
58
|
+
|
59
|
+
rescue ArgumentError => e
|
60
|
+
# Configuration errors (missing ENV vars, invalid formats)
|
61
|
+
if config&.json_only
|
62
|
+
puts JSON.pretty_generate({ error: e.message })
|
63
|
+
else
|
64
|
+
puts "Error: #{e.message}"
|
65
|
+
puts "Use --help for usage information"
|
66
|
+
end
|
67
|
+
exit(1)
|
68
|
+
rescue => e
|
69
|
+
# Catch other potential setup errors
|
70
|
+
if config&.json_only
|
71
|
+
puts JSON.pretty_generate({ error: e.message })
|
72
|
+
else
|
73
|
+
puts "Initialization Error: #{e.message}"
|
74
|
+
puts e.backtrace.join("\n")
|
75
|
+
end
|
76
|
+
exit(1)
|
77
|
+
end
|
78
|
+
|
79
|
+
# --- Main Execution ---
|
80
|
+
begin
|
81
|
+
# Only output this in verbose mode
|
82
|
+
logger.info("Results will be output directly") unless config.json_only
|
83
|
+
|
84
|
+
# Create the runner with our configuration
|
85
|
+
runner = GithubDailyDigest::DailyDigestRunner.new(config: config, logger: logger)
|
86
|
+
|
87
|
+
# Run and get the results
|
88
|
+
results = runner.run
|
89
|
+
|
90
|
+
# Output results in proper format and destination
|
91
|
+
output_formatter = GithubDailyDigest::OutputFormatter.new(config: config, logger: logger)
|
92
|
+
formatted_output = output_formatter.format(results)
|
93
|
+
|
94
|
+
# Send to correct destination
|
95
|
+
if config.output_to_stdout
|
96
|
+
# Just output to stdout
|
97
|
+
puts formatted_output
|
98
|
+
else
|
99
|
+
# Save to a log file
|
100
|
+
results_dir = File.expand_path('results', Dir.pwd)
|
101
|
+
FileUtils.mkdir_p(results_dir)
|
102
|
+
|
103
|
+
# Create appropriate filename with format extension
|
104
|
+
format_ext = config.output_format == 'markdown' ? 'md' : 'json'
|
105
|
+
results_file = File.join(results_dir, "daily_digest_#{Time.now.strftime('%Y%m%d_%H%M%S')}.#{format_ext}")
|
106
|
+
File.write(results_file, formatted_output)
|
107
|
+
logger.info("Results saved to #{results_file}")
|
108
|
+
end
|
109
|
+
|
110
|
+
logger.info("Execution finished successfully.") unless config.json_only
|
111
|
+
|
112
|
+
exit(0)
|
113
|
+
rescue => e
|
114
|
+
# Handle errors during execution
|
115
|
+
if config&.output_formats&.include?('markdown')
|
116
|
+
error_output = "# Error\n\n"
|
117
|
+
error_output << "**Error Message:** #{e.message}\n\n"
|
118
|
+
error_output << "**Stack Trace:**\n\n```\n#{e.backtrace.join("\n")}\n```\n" unless config&.json_only
|
119
|
+
|
120
|
+
if config&.output_to_stdout
|
121
|
+
puts error_output
|
122
|
+
else
|
123
|
+
# Try to write to a log file
|
124
|
+
begin
|
125
|
+
results_dir = File.expand_path('results', Dir.pwd)
|
126
|
+
FileUtils.mkdir_p(results_dir)
|
127
|
+
results_file = File.join(results_dir, "error_#{Time.now.strftime('%Y%m%d_%H%M%S')}.md")
|
128
|
+
File.write(results_file, error_output)
|
129
|
+
puts "Error details saved to #{results_file}"
|
130
|
+
rescue
|
131
|
+
puts error_output
|
132
|
+
end
|
133
|
+
end
|
134
|
+
else # Default to JSON
|
135
|
+
error_json = { error: e.message }
|
136
|
+
error_json[:backtrace] = e.backtrace unless config&.json_only
|
137
|
+
puts JSON.pretty_generate(error_json)
|
138
|
+
end
|
139
|
+
exit(1)
|
140
|
+
end
|
data/bin/setup
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
# github_daily_digest/github-daily-digest.gemspec
|
2
|
+
require_relative "lib/github-daily-digest/version"
|
3
|
+
|
4
|
+
Gem::Specification.new do |spec|
|
5
|
+
spec.name = "github-daily-digest"
|
6
|
+
spec.version = GithubDailyDigest::VERSION
|
7
|
+
spec.authors = ["Arturas Piksrys"]
|
8
|
+
spec.email = ["arturas@wisemonks.com"]
|
9
|
+
|
10
|
+
spec.summary = "Generates daily activity digests for GitHub organization members using Gemini."
|
11
|
+
spec.description = "Fetches recent GitHub commits and PR reviews for organization members and uses Google's Gemini API to analyze and summarize the activity. Provides an executable for daily runs."
|
12
|
+
spec.homepage = "https://github.com/wisemonks/github-daily-digest"
|
13
|
+
spec.license = "MIT"
|
14
|
+
spec.required_ruby_version = ">= 2.7.0"
|
15
|
+
|
16
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
17
|
+
spec.metadata["source_code_uri"] = spec.homepage # Assumes source is at homepage
|
18
|
+
# spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here." # Optional
|
19
|
+
|
20
|
+
# Specify which files should be added to the gem when it is packaged.
|
21
|
+
# Note: Excludes test files, .env, logs, results, etc.
|
22
|
+
spec.files = Dir[
|
23
|
+
'lib/**/*',
|
24
|
+
'bin/*',
|
25
|
+
'README.md',
|
26
|
+
'LICENSE',
|
27
|
+
'Rakefile',
|
28
|
+
'github-daily-digest.gemspec',
|
29
|
+
'github-daily-digest.rb'
|
30
|
+
]
|
31
|
+
spec.bindir = "bin" # Directory for executables
|
32
|
+
spec.executables = spec.files.grep(%r{\Abin/}) { |f| File.basename(f) } # Find executables in bin/
|
33
|
+
spec.require_paths = ["lib"] # Code is loaded from the lib directory
|
34
|
+
|
35
|
+
# Runtime Dependencies: Gems required for the gem to function
|
36
|
+
spec.add_dependency "activesupport", "~> 7.0" # Or your preferred version constraint
|
37
|
+
spec.add_dependency "gemini-ai", "~> 4.2"
|
38
|
+
spec.add_dependency "logger" # Standard lib, but good practice to list
|
39
|
+
spec.add_dependency "octokit", "~> 6.1"
|
40
|
+
spec.add_dependency "graphql-client", "~> 0.19.0"
|
41
|
+
|
42
|
+
# Development Dependencies: Gems needed for development/testing (managed by Gemfile)
|
43
|
+
spec.add_development_dependency "bundler", ">= 2.0"
|
44
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
45
|
+
# spec.add_development_dependency "rspec", "~> 3.0" # If you add tests
|
46
|
+
spec.add_development_dependency "dotenv", "~> 2.8" # Needed by the *executable* for convenience
|
47
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# github_daily_digest/lib/github_daily_digest.rb
|
2
|
+
require "logger"
|
3
|
+
require "active_support/all"
|
4
|
+
require "octokit"
|
5
|
+
require "gemini-ai"
|
6
|
+
require "json"
|
7
|
+
require "time"
|
8
|
+
|
9
|
+
# Order matters for relative requires if classes depend on each other during load
|
10
|
+
require_relative "lib/github_daily_digest/version"
|
11
|
+
require_relative "lib/configuration"
|
12
|
+
require_relative "lib/github_service"
|
13
|
+
require_relative "lib/github_graphql_service"
|
14
|
+
require_relative "lib/gemini_service"
|
15
|
+
require_relative "lib/activity_analyzer"
|
16
|
+
require_relative "lib/daily_digest_runner"
|
17
|
+
|
18
|
+
module GithubDailyDigest
|
19
|
+
class Error < StandardError; end
|
20
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# github_daily_digest/lib/activity_analyzer.rb
|
2
|
+
require_relative './language_analyzer.rb'
|
3
|
+
module GithubDailyDigest
|
4
|
+
class ActivityAnalyzer
|
5
|
+
def initialize(gemini_service:, github_graphql_service:, logger:)
|
6
|
+
@gemini_service = gemini_service
|
7
|
+
@github_graphql_service = github_graphql_service
|
8
|
+
@logger = logger
|
9
|
+
end
|
10
|
+
|
11
|
+
def analyze(username:, activity_data:, time_window_days: 7)
|
12
|
+
@logger.info("Analyzing activity for user: #{username}")
|
13
|
+
commits = activity_data[:commits]
|
14
|
+
review_count = activity_data[:review_count]
|
15
|
+
|
16
|
+
commits_with_code = if @github_graphql_service
|
17
|
+
@github_graphql_service.fetch_commits_changes(commits)
|
18
|
+
else
|
19
|
+
@logger.debug("GraphQL service not available, proceeding without detailed commit changes")
|
20
|
+
commits
|
21
|
+
end
|
22
|
+
|
23
|
+
analysis_result = @gemini_service.analyze_activity(
|
24
|
+
username: username,
|
25
|
+
commits_with_code: commits_with_code,
|
26
|
+
review_count: review_count,
|
27
|
+
time_window_days: time_window_days
|
28
|
+
)
|
29
|
+
@logger.debug("Gemini analysis result for #{username}: #{analysis_result}")
|
30
|
+
|
31
|
+
# Add username and timestamp to the result for context
|
32
|
+
analysis_result[:username] = username
|
33
|
+
analysis_result[:analysis_timestamp] = Time.now.iso8601
|
34
|
+
|
35
|
+
# Calculate language distribution
|
36
|
+
|
37
|
+
analysis_result[:language_distribution] = LanguageAnalyzer.calculate_distribution(commits_with_code.map{ |f| f[:code_changes][:files] }.flatten)
|
38
|
+
|
39
|
+
if analysis_result[:error]
|
40
|
+
@logger.error("Analysis failed for #{username}: #{analysis_result[:error]}")
|
41
|
+
else
|
42
|
+
@logger.info("Analysis successful for #{username}")
|
43
|
+
end
|
44
|
+
|
45
|
+
analysis_result
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,260 @@
|
|
1
|
+
# github_daily_digest/lib/configuration.rb
|
2
|
+
require 'dotenv'
|
3
|
+
require 'active_support/all' # For parsing duration string
|
4
|
+
require 'optparse'
|
5
|
+
|
6
|
+
module GithubDailyDigest
|
7
|
+
class Configuration
|
8
|
+
attr_reader :github_token, :gemini_api_key, :github_org_name, :github_org_names,
|
9
|
+
:log_level, :fetch_window_duration, :max_api_retries,
|
10
|
+
:rate_limit_sleep_base, :time_since, :gemini_model,
|
11
|
+
:json_only, :output_to_stdout, :help_requested,
|
12
|
+
:output_formats, :output_destination, :concise_output,
|
13
|
+
:use_graphql, :no_graphql, :specific_users,
|
14
|
+
:html_theme, :html_title, :html_show_charts,
|
15
|
+
:time_window_days
|
16
|
+
|
17
|
+
def initialize(args = nil)
|
18
|
+
Dotenv.load
|
19
|
+
@json_only = true # Default to JSON only output
|
20
|
+
@output_to_stdout = true # Default to stdout
|
21
|
+
@gemini_model = ENV.fetch('GEMINI_MODEL', 'gemini-1.5-flash')
|
22
|
+
@help_requested = false
|
23
|
+
|
24
|
+
# Support multiple output formats - if env specifies a single format, convert to array
|
25
|
+
output_format_env = ENV.fetch('OUTPUT_FORMAT', 'json').downcase
|
26
|
+
@output_formats = output_format_env.include?(',') ?
|
27
|
+
output_format_env.split(',').map(&:strip).map(&:downcase) :
|
28
|
+
[output_format_env.downcase]
|
29
|
+
|
30
|
+
@output_destination = ENV.fetch('OUTPUT_DESTINATION', 'stdout').downcase # 'stdout' or 'log'
|
31
|
+
@concise_output = ENV.fetch('CONCISE_OUTPUT', 'true').downcase == 'true' # Whether to use concise output format (defaults to true)
|
32
|
+
@use_graphql = ENV.fetch('USE_GRAPHQL', 'true').downcase == 'true' # Whether to use GraphQL API (defaults to true)
|
33
|
+
@no_graphql = !@use_graphql # Inverse of use_graphql for easier API
|
34
|
+
@specific_users = ENV.fetch('SPECIFIC_USERS', '').split(',').map(&:strip).reject(&:empty?) # Specific users to process
|
35
|
+
|
36
|
+
# HTML-specific options
|
37
|
+
@html_theme = ENV.fetch('HTML_THEME', 'default').downcase # 'default', 'dark', or 'light'
|
38
|
+
@html_title = ENV.fetch('HTML_TITLE', nil) # Custom title for HTML output
|
39
|
+
@html_show_charts = ENV.fetch('HTML_SHOW_CHARTS', 'true').downcase == 'true' # Whether to show charts in HTML output
|
40
|
+
|
41
|
+
# Parse command line arguments if provided
|
42
|
+
parse_command_line_args(args) if args
|
43
|
+
|
44
|
+
# Early return if help is requested
|
45
|
+
return if @help_requested
|
46
|
+
|
47
|
+
# Load environment variables
|
48
|
+
load_env_vars
|
49
|
+
|
50
|
+
# Validate the configuration
|
51
|
+
validate_config
|
52
|
+
|
53
|
+
# Calculate the time window
|
54
|
+
calculate_time_window
|
55
|
+
end
|
56
|
+
|
57
|
+
def help_text
|
58
|
+
<<~HELP
|
59
|
+
USAGE: github-daily-digest [options]
|
60
|
+
|
61
|
+
GitHub Daily Digest generates insights about developer activity using GitHub API and Google's Gemini AI.
|
62
|
+
|
63
|
+
Options:
|
64
|
+
-h, --help Show this help message
|
65
|
+
-t, --token TOKEN GitHub API token (instead of GITHUB_TOKEN env var)
|
66
|
+
-g, --gemini-key KEY Gemini API key (instead of GEMINI_API_KEY env var)
|
67
|
+
-o, --org NAME GitHub organization name(s) (instead of GITHUB_ORG_NAME env var)
|
68
|
+
Multiple organizations can be comma-separated (e.g., 'org1,org2,org3')
|
69
|
+
-u, --users USERNAMES Specific users to process (comma-separated, e.g. 'user1,user2,user3')
|
70
|
+
If not specified, all organization members will be processed
|
71
|
+
-m, --model MODEL Gemini model to use (default: gemini-1.5-flash)
|
72
|
+
-w, --window DURATION Time window for fetching data (e.g., '1.day', '12.hours')
|
73
|
+
-v, --verbose Enable verbose output (instead of JSON-only)
|
74
|
+
-l, --log-level LEVEL Set log level (DEBUG, INFO, WARN, ERROR, FATAL)
|
75
|
+
-f, --format FORMAT Output format: json, markdown, html (default: json)
|
76
|
+
html option generates a standalone web page with charts
|
77
|
+
-d, --destination DEST Output destination: stdout or log (default: stdout)
|
78
|
+
-c, --[no-]concise Use concise output format (overview + combined only, default: true)
|
79
|
+
-q, --[no-]graphql Use GraphQL API for better performance and data quality (default: true)
|
80
|
+
Use --no-graphql to fall back to REST API
|
81
|
+
|
82
|
+
HTML Output Options:
|
83
|
+
--html-theme THEME Theme for HTML output: default, dark, light (default: default)
|
84
|
+
--html-title TITLE Custom title for HTML output
|
85
|
+
--[no-]html-charts Include interactive charts in HTML output (default: true)
|
86
|
+
|
87
|
+
Examples:
|
88
|
+
github-daily-digest --token YOUR_TOKEN --gemini-key YOUR_KEY --org acme-inc
|
89
|
+
github-daily-digest --window 2.days --verbose
|
90
|
+
github-daily-digest --format markdown --destination log
|
91
|
+
github-daily-digest --org "org1,org2,org3" --format markdown
|
92
|
+
|
93
|
+
Environment variables can also be used instead of command-line options:
|
94
|
+
GITHUB_TOKEN, GEMINI_API_KEY, GITHUB_ORG_NAME, GEMINI_MODEL, FETCH_WINDOW, LOG_LEVEL,
|
95
|
+
OUTPUT_FORMAT, OUTPUT_DESTINATION, CONCISE_OUTPUT, USE_GRAPHQL
|
96
|
+
HELP
|
97
|
+
end
|
98
|
+
|
99
|
+
# Check if we need to show help
|
100
|
+
def help_requested?
|
101
|
+
@help_requested
|
102
|
+
end
|
103
|
+
|
104
|
+
# Returns number of days in the configured time window
|
105
|
+
def time_window_days
|
106
|
+
if @fetch_window_duration
|
107
|
+
# Handle ActiveSupport::Duration objects
|
108
|
+
if @fetch_window_duration.respond_to?(:in_days)
|
109
|
+
return @fetch_window_duration.in_days.to_i
|
110
|
+
# Handle string durations like '7.days'
|
111
|
+
elsif @fetch_window_duration.is_a?(String)
|
112
|
+
match = @fetch_window_duration.match(/(\d+)\.days/)
|
113
|
+
return match ? match[1].to_i : 7
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Default to 7 days if fetch_window_duration not set or has unexpected type
|
118
|
+
return 7
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
def parse_command_line_args(args)
|
124
|
+
parser = OptionParser.new do |opts|
|
125
|
+
opts.banner = "Usage: github-daily-digest [options]"
|
126
|
+
|
127
|
+
opts.on("-h", "--help", "Show help") do
|
128
|
+
puts help_text
|
129
|
+
@help_requested = true
|
130
|
+
end
|
131
|
+
|
132
|
+
opts.on("-t", "--token TOKEN", "GitHub API token") do |token|
|
133
|
+
ENV['GITHUB_TOKEN'] = token
|
134
|
+
end
|
135
|
+
|
136
|
+
opts.on("-g", "--gemini-key KEY", "Gemini API key") do |key|
|
137
|
+
ENV['GEMINI_API_KEY'] = key
|
138
|
+
end
|
139
|
+
|
140
|
+
opts.on("-o", "--org NAME", "GitHub organization name(s)") do |org|
|
141
|
+
ENV['GITHUB_ORG_NAME'] = org
|
142
|
+
end
|
143
|
+
|
144
|
+
opts.on("-u", "--users USERNAMES", "Specific users to process (comma-separated)") do |users|
|
145
|
+
@specific_users = users.split(',').map(&:strip).reject(&:empty?)
|
146
|
+
end
|
147
|
+
|
148
|
+
opts.on("-m", "--model MODEL", "Gemini model to use") do |model|
|
149
|
+
@gemini_model = model
|
150
|
+
end
|
151
|
+
|
152
|
+
opts.on("-w", "--window DURATION", "Time window for fetching data (e.g., '1.day')") do |window|
|
153
|
+
ENV['FETCH_WINDOW'] = window
|
154
|
+
end
|
155
|
+
|
156
|
+
opts.on("-v", "--verbose", "Enable verbose output (instead of JSON-only)") do
|
157
|
+
@json_only = false
|
158
|
+
end
|
159
|
+
|
160
|
+
opts.on("-l", "--log-level LEVEL", "Set log level (DEBUG, INFO, WARN, ERROR, FATAL)") do |level|
|
161
|
+
ENV['LOG_LEVEL'] = level
|
162
|
+
end
|
163
|
+
|
164
|
+
opts.on("-f", "--format FORMAT", "Output format (json, markdown, html)") do |format|
|
165
|
+
@output_formats = format.downcase.split(',').map(&:strip).map(&:downcase)
|
166
|
+
unless @output_formats.all? { |f| ['json', 'markdown', 'html'].include?(f) }
|
167
|
+
puts "Error: Invalid output format(s) '#{format}'. Must be one or more of 'json', 'markdown', or 'html'."
|
168
|
+
exit(1)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
opts.on("-d", "--destination DEST", "Output destination (stdout, log)") do |dest|
|
173
|
+
@output_destination = dest.downcase
|
174
|
+
unless ['stdout', 'log'].include?(@output_destination)
|
175
|
+
puts "Error: Invalid output destination '#{dest}'. Must be 'stdout' or 'log'."
|
176
|
+
exit(1)
|
177
|
+
end
|
178
|
+
@output_to_stdout = (@output_destination == 'stdout')
|
179
|
+
end
|
180
|
+
|
181
|
+
opts.on("-c", "--[no-]concise", "Use concise output format (overview + combined only)") do |concise|
|
182
|
+
@concise_output = concise
|
183
|
+
end
|
184
|
+
|
185
|
+
opts.on("-q", "--[no-]graphql", "Use GraphQL API for better performance and data quality") do |graphql|
|
186
|
+
@use_graphql = graphql
|
187
|
+
@no_graphql = !graphql
|
188
|
+
end
|
189
|
+
|
190
|
+
# HTML-specific options
|
191
|
+
opts.on("--html-theme THEME", "Theme for HTML output (default, dark, light)") do |theme|
|
192
|
+
unless ['default', 'dark', 'light'].include?(theme.downcase)
|
193
|
+
puts "Error: Invalid HTML theme '#{theme}'. Must be 'default', 'dark', or 'light'."
|
194
|
+
exit(1)
|
195
|
+
end
|
196
|
+
@html_theme = theme.downcase
|
197
|
+
end
|
198
|
+
|
199
|
+
opts.on("--html-title TITLE", "Custom title for HTML output") do |title|
|
200
|
+
@html_title = title
|
201
|
+
end
|
202
|
+
|
203
|
+
opts.on("--[no-]html-charts", "Include interactive charts in HTML output") do |show_charts|
|
204
|
+
@html_show_charts = show_charts
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
begin
|
209
|
+
parser.parse!(args)
|
210
|
+
rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e
|
211
|
+
puts "Error: #{e.message}"
|
212
|
+
puts parser
|
213
|
+
exit(1)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
def load_env_vars
|
218
|
+
@github_token = ENV['GITHUB_TOKEN']
|
219
|
+
@gemini_api_key = ENV['GEMINI_API_KEY']
|
220
|
+
@github_org_name = ENV['GITHUB_ORG_NAME']
|
221
|
+
# Parse multiple organizations if provided (comma-separated)
|
222
|
+
@github_org_names = @github_org_name.split(',').map(&:strip) if @github_org_name
|
223
|
+
@log_level = ENV.fetch('LOG_LEVEL', 'INFO').upcase
|
224
|
+
@fetch_window_duration_str = ENV.fetch('FETCH_WINDOW', '7.days')
|
225
|
+
@max_api_retries = ENV.fetch('MAX_API_RETRIES', '3').to_i
|
226
|
+
@rate_limit_sleep_base = ENV.fetch('RATE_LIMIT_SLEEP_BASE', '5').to_i
|
227
|
+
end
|
228
|
+
|
229
|
+
def validate_config
|
230
|
+
return if @help_requested
|
231
|
+
|
232
|
+
raise ArgumentError, "Missing required env var or option: GITHUB_TOKEN" unless @github_token
|
233
|
+
raise ArgumentError, "Missing required env var or option: GEMINI_API_KEY" unless @gemini_api_key
|
234
|
+
raise ArgumentError, "Missing required env var or option: GITHUB_ORG_NAME" unless @github_org_name
|
235
|
+
validate_log_level
|
236
|
+
parse_fetch_window
|
237
|
+
end
|
238
|
+
|
239
|
+
def validate_log_level
|
240
|
+
valid_levels = %w[DEBUG INFO WARN ERROR FATAL]
|
241
|
+
unless valid_levels.include?(@log_level)
|
242
|
+
raise ArgumentError, "Invalid LOG_LEVEL: #{@log_level}. Must be one of #{valid_levels.join(', ')}"
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
def parse_fetch_window
|
247
|
+
# Attempt to parse the duration string (e.g., "1.day", "24.hours")
|
248
|
+
@fetch_window_duration = eval(@fetch_window_duration_str) # Use eval carefully here
|
249
|
+
unless @fetch_window_duration.is_a?(ActiveSupport::Duration)
|
250
|
+
raise ArgumentError, "Invalid FETCH_WINDOW format: '#{@fetch_window_duration_str}'. Use ActiveSupport format (e.g., '1.day', '24.hours')."
|
251
|
+
end
|
252
|
+
rescue SyntaxError, NameError => e
|
253
|
+
raise ArgumentError, "Error parsing FETCH_WINDOW: '#{@fetch_window_duration_str}'. #{e.message}"
|
254
|
+
end
|
255
|
+
|
256
|
+
def calculate_time_window
|
257
|
+
@time_since = (@fetch_window_duration.ago).iso8601
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|