caperoma 0.1.0 → 4.0.1

Sign up to get free protection for your applications and to get access to all the features.
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