caperoma 0.1.0 → 4.0.1

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 (85) hide show
  1. checksums.yaml +5 -5
  2. data/.ruby-version +1 -0
  3. data/Capefile +48 -0
  4. data/Capefile.template +48 -0
  5. data/Capefile.test +20 -0
  6. data/Gemfile +25 -10
  7. data/Gemfile.lock +196 -77
  8. data/HELP +321 -0
  9. data/README.md +528 -0
  10. data/Rakefile +73 -18
  11. data/VERSION +1 -1
  12. data/bin/caperoma +47 -11
  13. data/caperoma.gemspec +144 -45
  14. data/config/crontab +10 -0
  15. data/config/schedule.rb +21 -0
  16. data/lib/caperoma.rb +409 -9
  17. data/lib/caperoma/models/account.rb +47 -0
  18. data/lib/caperoma/models/application_record.rb +5 -0
  19. data/lib/caperoma/models/branch.rb +6 -0
  20. data/lib/caperoma/models/project.rb +14 -0
  21. data/lib/caperoma/models/property.rb +5 -0
  22. data/lib/caperoma/models/report.rb +177 -0
  23. data/lib/caperoma/models/report_recipient.rb +6 -0
  24. data/lib/caperoma/models/reports/daily_report.rb +23 -0
  25. data/lib/caperoma/models/reports/retrospective_report.rb +19 -0
  26. data/lib/caperoma/models/reports/three_day_report.rb +19 -0
  27. data/lib/caperoma/models/task.rb +368 -0
  28. data/lib/caperoma/models/tasks/bug.rb +36 -0
  29. data/lib/caperoma/models/tasks/chore.rb +40 -0
  30. data/lib/caperoma/models/tasks/feature.rb +27 -0
  31. data/lib/caperoma/models/tasks/fix.rb +56 -0
  32. data/lib/caperoma/models/tasks/meeting.rb +40 -0
  33. data/lib/caperoma/models/tasks/modules/git.rb +65 -0
  34. data/lib/caperoma/models/tasks/task_with_commit.rb +40 -0
  35. data/lib/caperoma/models/tasks/task_with_separate_branch.rb +42 -0
  36. data/lib/caperoma/services/airbrake_email_processor.rb +47 -0
  37. data/lib/caperoma/services/pivotal_fetcher.rb +108 -0
  38. data/lib/caperoma/version.rb +9 -0
  39. data/spec/caperoma_spec.rb +3 -21
  40. data/spec/factories/accounts.rb +10 -0
  41. data/spec/factories/branches.rb +9 -0
  42. data/spec/factories/projects.rb +8 -0
  43. data/spec/factories/report_recipients.rb +7 -0
  44. data/spec/factories/reports.rb +16 -0
  45. data/spec/factories/tasks.rb +37 -0
  46. data/spec/features/bug_spec.rb +60 -0
  47. data/spec/features/chore_spec.rb +60 -0
  48. data/spec/features/command_unknown_spec.rb +14 -0
  49. data/spec/features/config_spec.rb +161 -0
  50. data/spec/features/feature_spec.rb +60 -0
  51. data/spec/features/finish_spec.rb +18 -0
  52. data/spec/features/fix_spec.rb +60 -0
  53. data/spec/features/meeting_spec.rb +22 -0
  54. data/spec/features/projects_spec.rb +17 -0
  55. data/spec/features/report_recipientss_spec.rb +117 -0
  56. data/spec/features/reports_spec.rb +65 -0
  57. data/spec/features/status_spec.rb +33 -0
  58. data/spec/features/version_spec.rb +11 -0
  59. data/spec/models/account_spec.rb +51 -0
  60. data/spec/models/branch_spec.rb +8 -0
  61. data/spec/models/bug_spec.rb +33 -0
  62. data/spec/models/chore_spec.rb +33 -0
  63. data/spec/models/daily_report_spec.rb +38 -0
  64. data/spec/models/feature_spec.rb +33 -0
  65. data/spec/models/fix_spec.rb +55 -0
  66. data/spec/models/meeting_spec.rb +33 -0
  67. data/spec/models/project_spec.rb +11 -0
  68. data/spec/models/report_recipient_spec.rb +22 -0
  69. data/spec/models/report_spec.rb +16 -0
  70. data/spec/models/retrospective_report_spec.rb +38 -0
  71. data/spec/models/task_spec.rb +613 -0
  72. data/spec/models/task_with_commit_spec.rb +105 -0
  73. data/spec/models/task_with_separate_branch_spec.rb +97 -0
  74. data/spec/models/three_day_report_spec.rb +49 -0
  75. data/spec/spec_helper.rb +26 -16
  76. data/spec/support/capefile_generator.rb +36 -0
  77. data/spec/support/database_cleaner.rb +21 -0
  78. data/spec/support/stubs.rb +178 -9
  79. metadata +283 -42
  80. data/.document +0 -5
  81. data/README.rdoc +0 -26
  82. data/lib/caperoma/credentials.rb +0 -13
  83. data/lib/caperoma/jira_client.rb +0 -57
  84. data/spec/caperoma/credentials_spec.rb +0 -25
  85. data/spec/caperoma/jira_spec.rb +0 -35
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Bug < TaskWithSeparateBranch
4
+ before_create :inform_creation_started
5
+ after_create :inform_creation_finished
6
+
7
+ private
8
+
9
+ def create_issue_on_pivotal_data
10
+ Jbuilder.encode do |j|
11
+ j.current_state 'unstarted'
12
+ j.name title.to_s
13
+ j.story_type story_type
14
+ end
15
+ end
16
+
17
+ def this_is_a_type_a_user_wants_to_create?
18
+ project.create_bugs_in_pivotal
19
+ end
20
+
21
+ def story_type
22
+ 'bug'
23
+ end
24
+
25
+ def issue_type
26
+ project.bug_jira_task_id
27
+ end
28
+
29
+ def inform_creation_started
30
+ puts 'Starting a new bug'
31
+ end
32
+
33
+ def inform_creation_finished
34
+ puts 'A new bug started'
35
+ end
36
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+ class Chore < Task
3
+ before_create :inform_creation_started
4
+ after_create :inform_creation_finished
5
+
6
+ private
7
+ def create_issue_on_pivotal_data
8
+ Jbuilder.encode do |j|
9
+ j.current_state 'unstarted'
10
+ j.name title.to_s
11
+ j.story_type story_type
12
+ end
13
+ end
14
+
15
+ def finish_on_pivotal_data
16
+ Jbuilder.encode do |j|
17
+ j.current_state 'accepted'
18
+ end
19
+ end
20
+
21
+ def this_is_a_type_a_user_wants_to_create?
22
+ project.create_chores_in_pivotal
23
+ end
24
+
25
+ def story_type
26
+ 'chore'
27
+ end
28
+
29
+ def issue_type
30
+ project.chore_jira_task_id
31
+ end
32
+
33
+ def inform_creation_started
34
+ puts 'Starting a new chore'
35
+ end
36
+
37
+ def inform_creation_finished
38
+ puts 'A new chore started'
39
+ end
40
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+ class Feature < TaskWithSeparateBranch
3
+ before_create :inform_creation_started
4
+ after_create :inform_creation_finished
5
+
6
+ private
7
+
8
+ def this_is_a_type_a_user_wants_to_create?
9
+ project.create_features_in_pivotal
10
+ end
11
+
12
+ def story_type
13
+ 'feature'
14
+ end
15
+
16
+ def issue_type
17
+ project.feature_jira_task_id
18
+ end
19
+
20
+ def inform_creation_started
21
+ puts 'Starting a new feature'
22
+ end
23
+
24
+ def inform_creation_finished
25
+ puts 'A new feature started'
26
+ end
27
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+ class Fix < TaskWithCommit
3
+ before_create :update_parent_branch
4
+ before_create :inform_creation_started
5
+ after_create :inform_creation_finished
6
+
7
+ def description
8
+ result = super
9
+ last_commit = git_last_commit_name
10
+ "#{result}\n(For: #{last_commit})"
11
+ end
12
+
13
+ def finish(comment)
14
+ git_rebase_to_upstream
15
+ super
16
+ end
17
+
18
+ private
19
+ def create_issue_on_pivotal_data
20
+ Jbuilder.encode do |j|
21
+ j.current_state 'unstarted'
22
+ j.name title.to_s
23
+ j.story_type story_type
24
+ end
25
+ end
26
+
27
+ def finish_on_pivotal_data
28
+ Jbuilder.encode do |j|
29
+ j.current_state 'accepted'
30
+ end
31
+ end
32
+
33
+ def this_is_a_type_a_user_wants_to_create?
34
+ project.create_fixes_in_pivotal_as_chores
35
+ end
36
+
37
+ def story_type
38
+ 'chore'
39
+ end
40
+
41
+ def update_parent_branch
42
+ git_rebase_to_upstream
43
+ end
44
+
45
+ def issue_type
46
+ project.fix_jira_task_id
47
+ end
48
+
49
+ def inform_creation_started
50
+ puts 'Starting a new fix'
51
+ end
52
+
53
+ def inform_creation_finished
54
+ puts 'A new fix started'
55
+ end
56
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+ class Meeting < Task
3
+ before_create :inform_creation_started
4
+ after_create :inform_creation_finished
5
+
6
+ private
7
+ def create_issue_on_pivotal_data
8
+ Jbuilder.encode do |j|
9
+ j.current_state 'unstarted'
10
+ j.name title.to_s
11
+ j.story_type story_type
12
+ end
13
+ end
14
+
15
+ def finish_on_pivotal_data
16
+ Jbuilder.encode do |j|
17
+ j.current_state 'accepted'
18
+ end
19
+ end
20
+
21
+ def this_is_a_type_a_user_wants_to_create?
22
+ project.create_meetings_in_pivotal_as_chores
23
+ end
24
+
25
+ def story_type
26
+ 'chore'
27
+ end
28
+
29
+ def issue_type
30
+ project.meeting_jira_task_id
31
+ end
32
+
33
+ def inform_creation_started
34
+ puts 'Starting a new meeting'
35
+ end
36
+
37
+ def inform_creation_finished
38
+ puts 'A new meeting started'
39
+ end
40
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Git
4
+ def git_branch(name)
5
+ `git -C "#{project.folder_path}" checkout -b #{name}` if ENV['CAPEROMA_INTEGRATION_TEST'].blank? && ENV['CAPEROMA_TEST'].blank?
6
+ end
7
+
8
+ def git_commit(msg)
9
+ `git -C "#{project.folder_path}" add -A && git -C "#{project.folder_path}" commit --allow-empty -m "#{msg}"` if ENV['CAPEROMA_INTEGRATION_TEST'].blank? && ENV['CAPEROMA_TEST'].blank?
10
+ end
11
+
12
+ def git_push
13
+ `git -C "#{project.folder_path}" push --set-upstream origin #{git_current_branch}` if ENV['CAPEROMA_INTEGRATION_TEST'].blank? && ENV['CAPEROMA_TEST'].blank?
14
+ end
15
+
16
+ def git_last_commit_name
17
+ `git -C "#{project.folder_path}" log --pretty=format:'%s' -1` if ENV['CAPEROMA_INTEGRATION_TEST'].blank? && ENV['CAPEROMA_TEST'].blank?
18
+ end
19
+
20
+ def git_current_branch
21
+ `git -C "#{project.folder_path}" rev-parse --abbrev-ref HEAD`.gsub("\n", '') if ENV['CAPEROMA_INTEGRATION_TEST'].blank? && ENV['CAPEROMA_TEST'].blank?
22
+ end
23
+
24
+ def git_pull_request(into, title, description = '')
25
+ pull_request_data = Jbuilder.encode do |j|
26
+ j.title title
27
+ j.body description
28
+ j.head git_current_branch
29
+ j.base into
30
+ end
31
+
32
+ conn = Faraday.new(url: 'https://api.github.com') do |c|
33
+ c.basic_auth(Account.git.email, Account.git.password)
34
+ c.adapter Faraday.default_adapter
35
+ end
36
+
37
+ conn.post do |request|
38
+ request.url "/repos/#{project.github_repo}/pulls"
39
+ request.body = pull_request_data
40
+ request.headers['User-Agent'] = 'Caperoma'
41
+ request.headers['Accept'] = 'application/vnd.github.v3+json'
42
+ request.headers['Content-Type'] = 'application/json'
43
+ end
44
+ end
45
+
46
+ def git_rebase_to_upstream
47
+ if ENV['CAPEROMA_INTEGRATION_TEST'].blank? && ENV['CAPEROMA_TEST'].blank?
48
+ has_untracked_files = !`git -C "#{project.folder_path}" ls-files --others --exclude-standard`.empty?
49
+ has_changes = !`git -C "#{project.folder_path}" diff`.empty?
50
+ has_staged_changes = !`git -C "#{project.folder_path}" diff HEAD`.empty?
51
+
52
+ changes_were_made = has_untracked_files || has_changes || has_staged_changes
53
+
54
+ `git -C "#{project.folder_path}" add -A && git -C "#{project.folder_path}" stash` if changes_were_made
55
+
56
+ `git -C "#{project.folder_path}" fetch && git -C "#{project.folder_path}" rebase $(git -C "#{project.folder_path}" rev-parse --abbrev-ref --symbolic-full-name @{u})`
57
+
58
+ `git -C "#{project.folder_path}" stash apply` if changes_were_made
59
+ end
60
+ end
61
+
62
+ def git_checkout(branch)
63
+ `git -C "#{project.folder_path}" checkout #{branch}` if ENV['CAPEROMA_INTEGRATION_TEST'].blank? && ENV['CAPEROMA_TEST'].blank?
64
+ end
65
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+ class TaskWithCommit < Task
3
+ belongs_to :branch
4
+
5
+ def finish(comment)
6
+ super
7
+ git_commit(commit_message)
8
+ # here I should pass the path
9
+ `rubocop -a "#{project.folder_path}"` if ENV['CAPEROMA_INTEGRATION_TEST'].blank? && ENV['CAPEROMA_TEST'].blank?
10
+ git_commit(commit_rubocop_message)
11
+ git_push
12
+ end
13
+
14
+ def pause(comment)
15
+ super
16
+ git_commit(commit_message)
17
+ `rubocop -a "#{project.folder_path}"` if ENV['CAPEROMA_INTEGRATION_TEST'].blank? && ENV['CAPEROMA_TEST'].blank?
18
+ git_commit(commit_rubocop_message)
19
+ git_push
20
+ end
21
+
22
+ private
23
+
24
+ def commit_message
25
+ # E.g.: [RUC-123][#1345231] Some Subject
26
+ string = ''
27
+ string += "[#{jira_key}]" if jira_key.present?
28
+ string += "[##{pivotal_id}]" if pivotal_id.present?
29
+ string += " #{title}"
30
+ string.strip
31
+ end
32
+
33
+ def commit_rubocop_message
34
+ string = ''
35
+ string += "[#{jira_key}]" if jira_key.present?
36
+ string += "[##{pivotal_id}]" if pivotal_id.present?
37
+ string += ' Applying good practices'
38
+ string.strip
39
+ end
40
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+ class TaskWithSeparateBranch < TaskWithCommit
3
+ before_create :update_parent_branch
4
+ before_create :remember_parent_branch
5
+ after_create :new_git_branch
6
+
7
+ def finish(comment)
8
+ puts comment
9
+ super
10
+ puts git_pull_request(parent_branch, title, description_for_pull_request)
11
+ puts git_checkout(parent_branch)
12
+ end
13
+
14
+ def abort(comment)
15
+ super
16
+ puts git_checkout(parent_branch)
17
+ end
18
+
19
+ private
20
+
21
+ def description_for_pull_request
22
+ pivotal_url
23
+ end
24
+
25
+ def update_parent_branch
26
+ git_rebase_to_upstream
27
+ end
28
+
29
+ def remember_parent_branch
30
+ self.parent_branch = git_current_branch
31
+ end
32
+
33
+ def new_git_branch
34
+ git_branch(branch_name)
35
+ end
36
+
37
+ def branch_name
38
+ # E.g.: ruc-123-first-three-four-words
39
+ result = [jira_key, title[0, 25]].join(' ')
40
+ ActiveSupport::Inflector.parameterize(result)
41
+ end
42
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AirbrakeEmailProcessor
4
+ # stub for test env
5
+ def process
6
+ account = Account.gmail
7
+ Gmail.new(account.email, account.password) do |gmail|
8
+ gmail.inbox.emails(:unread, from: 'donotreply@alerts.airbrake.io').select { |email| email.subject.include? '[Ruck.us]' }.each do |email|
9
+ # get reply address
10
+ reply_to = "#{email.to.first.mailbox}@#{email.to.first.host}"
11
+
12
+ # generate reply body
13
+ body = ''
14
+
15
+ story = PivotalFetcher.get_story_by_title(email.subject)
16
+ if story.present? && story.respond_to?(:url)
17
+ body = "Duplicate of: #{story.url}"
18
+ else
19
+ story = PivotalFetcher.create_story(email.subject, email.body.raw_source[0..1000])
20
+ body = "Created new story: #{story.url}"
21
+ end
22
+
23
+ # compose reply email (to get identifiers)
24
+ reply = email.reply do
25
+ subject "Re: #{email.subject}"
26
+ body body
27
+ end
28
+
29
+ # bugfix to make reply work
30
+ new_email = gmail.compose do
31
+ to reply_to # note it's generated email, not airbrake one
32
+ subject reply.subject
33
+ in_reply_to reply.in_reply_to
34
+ references reply.references
35
+ body reply.body.raw_source
36
+ end
37
+
38
+ # deliver reply
39
+ new_email.deliver!
40
+
41
+ # archive email
42
+ email.mark(:read)
43
+ email.archive!
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ class PivotalFetcher
4
+ def self.process(story_id)
5
+ story = get_story(story_id)
6
+
7
+ if story.present?
8
+ title = story.name
9
+ description = story.description
10
+ type = story.story_type
11
+ # labels = story.labels # in case I do Critical thing
12
+ # something liek if lables.include?("hot") pass "critical" status to Jira.
13
+
14
+ story.update(current_state: 'started', owned_by: 'Serge Vinogradoff')
15
+
16
+ args = [type, title, description, story_id] # or args = [type, title, description, "1", story_id] ? I use that 1 in older versions
17
+ case type
18
+ when 'feature'
19
+ Caperoma.feature(args)
20
+ when 'bug'
21
+ Caperoma.bug(args)
22
+ else
23
+ puts 'Unknown story type in Pivotal'
24
+ end
25
+
26
+ # copy Jira ID to Pivotal story
27
+ task = Task.where(pivotal_id: story_id).first
28
+ if task.present?
29
+ task.jira_key
30
+ story.notes.create(text: task.jira_key)
31
+ else
32
+ puts 'task does not exist'
33
+ end
34
+ else
35
+ puts 'Did not find a story'
36
+ end
37
+ end
38
+
39
+ def self.finish(story_id)
40
+ story = nil
41
+
42
+ PivotalTracker::Project.all.each do |project|
43
+ story = project.stories.find(story_id)
44
+ break if story.present?
45
+ end
46
+
47
+ story
48
+
49
+ if story.present? && story.tasks.all.empty?
50
+ story.update current_state: 'finished'
51
+ end
52
+ end
53
+
54
+ def self.create_story(title, description)
55
+ connect
56
+
57
+ project_id = 993_892
58
+
59
+ # this isn't needed anymore... these are PT ID's of Ruck.us.
60
+ if title.include? '[Ruck.us] Production '
61
+ project_id = 993_892
62
+ elsif title.include? '[Ruck.us] Staging '
63
+ project_id = 1_110_744
64
+ elsif title.include? '[Ruck.us] Staging2 '
65
+ project_id = 1_266_704
66
+ end
67
+
68
+ # TODO: icebox, need probably to move to backlog
69
+ project = PivotalTracker::Project.find(project_id)
70
+ story = project.stories.create name: title,
71
+ description: description,
72
+ requested_by: 'Serge Vinogradoff',
73
+ owned_by: 'Serge Vinogradoff',
74
+ story_type: 'bug'
75
+
76
+ story
77
+ end
78
+
79
+ def self.get_story_by_title(title)
80
+ connect
81
+
82
+ story = nil
83
+
84
+ PivotalTracker::Project.all.each do |project|
85
+ story = project.stories.all.select { |x| x.name == title }.first
86
+ break if story.present?
87
+ end
88
+
89
+ story
90
+ end
91
+
92
+ def self.get_story(story_id)
93
+ connect
94
+
95
+ story = nil
96
+
97
+ PivotalTracker::Project.all.each do |project|
98
+ story = project.stories.find(story_id)
99
+ break if story.present?
100
+ end
101
+
102
+ story
103
+ end
104
+
105
+ def self.connect
106
+ PivotalTracker::Client.token(Account.pivotal.email, Account.pivotal.password)
107
+ end
108
+ end