Class: Discharger::Task

Inherits:
Rake::TaskLib
  • Object
show all
Defined in:
lib/discharger/task.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name = :release, tasker: Rake::Task) ⇒ Task

Returns a new instance of Task.



49
50
51
52
53
54
55
56
# File 'lib/discharger/task.rb', line 49

def initialize(name = :release, tasker: Rake::Task)
  @name = name
  @tasker = tasker
  @working_branch = "develop"
  @staging_branch = "stage"
  @production_branch = "main"
  @description = "Release the current version to #{staging_branch}"
end

Instance Attribute Details

#app_nameObject

Returns the value of attribute app_name.



36
37
38
# File 'lib/discharger/task.rb', line 36

def app_name
  @app_name
end

#changelog_fileObject

Returns the value of attribute changelog_file.



44
45
46
# File 'lib/discharger/task.rb', line 44

def changelog_file
  @changelog_file
end

#chat_tokenObject

Returns the value of attribute chat_token.



35
36
37
# File 'lib/discharger/task.rb', line 35

def chat_token
  @chat_token
end

#commitObject

Returns the value of attribute commit.



46
47
48
# File 'lib/discharger/task.rb', line 46

def commit
  @commit
end

#commit_finalizeObject

Returns the value of attribute commit_finalize.



47
48
49
# File 'lib/discharger/task.rb', line 47

def commit_finalize
  @commit_finalize
end

#commit_identifierObject

Returns the value of attribute commit_identifier.



37
38
39
# File 'lib/discharger/task.rb', line 37

def commit_identifier
  @commit_identifier
end

#descriptionObject

Returns the value of attribute description.



26
27
28
# File 'lib/discharger/task.rb', line 26

def description
  @description
end

#nameObject

Returns the value of attribute name.



24
25
26
# File 'lib/discharger/task.rb', line 24

def name
  @name
end

#production_branchObject

Returns the value of attribute production_branch.



30
31
32
# File 'lib/discharger/task.rb', line 30

def production_branch
  @production_branch
end

#pull_request_urlObject

Returns the value of attribute pull_request_url.



38
39
40
# File 'lib/discharger/task.rb', line 38

def pull_request_url
  @pull_request_url
end

#release_message_channelObject

Returns the value of attribute release_message_channel.



32
33
34
# File 'lib/discharger/task.rb', line 32

def release_message_channel
  @release_message_channel
end

#staging_branchObject

Returns the value of attribute staging_branch.



29
30
31
# File 'lib/discharger/task.rb', line 29

def staging_branch
  @staging_branch
end

#updated_pathsObject

Returns the value of attribute updated_paths.



45
46
47
# File 'lib/discharger/task.rb', line 45

def updated_paths
  @updated_paths
end

#version_constantObject

Returns the value of attribute version_constant.



33
34
35
# File 'lib/discharger/task.rb', line 33

def version_constant
  @version_constant
end

#version_fileObject

Reissue settings



41
42
43
# File 'lib/discharger/task.rb', line 41

def version_file
  @version_file
end

#version_limitObject

Returns the value of attribute version_limit.



42
43
44
# File 'lib/discharger/task.rb', line 42

def version_limit
  @version_limit
end

#version_redo_procObject

Returns the value of attribute version_redo_proc.



43
44
45
# File 'lib/discharger/task.rb', line 43

def version_redo_proc
  @version_redo_proc
end

#working_branchObject

Returns the value of attribute working_branch.



28
29
30
# File 'lib/discharger/task.rb', line 28

def working_branch
  @working_branch
end

Class Method Details

.create(name = :release, tasker: Rake::Task, &block) ⇒ Object



8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# File 'lib/discharger/task.rb', line 8

def self.create(name = :release, tasker: Rake::Task, &block)
  task = new(name, tasker:)
  task.instance_eval(&block) if block
  Reissue::Task.create do |reissue|
    reissue.version_file = task.version_file
    reissue.version_limit = task.version_limit
    reissue.version_redo_proc = task.version_redo_proc
    reissue.changelog_file = task.changelog_file
    reissue.updated_paths = task.updated_paths
    reissue.commit = task.commit
    reissue.commit_finalize = task.commit_finalize
  end
  task.define
  task
end

Instance Method Details

#defineObject



111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
# File 'lib/discharger/task.rb', line 111

def define
  require "slack-ruby-client"
  Slack.configure do |config|
    config.token = chat_token
  end

  desc <<~DESC
    ---------- STEP 3 ----------
    Release the current version to production

    This task rebases the production branch on the staging branch and tags the
    current version. The production branch and the tag will be pushed to the
    remote repository.

    After the release is complete, a new branch will be created to bump the
    version for the next release.
  DESC
  task "#{name}": [:environment] do
    current_version = Object.const_get(version_constant)
    sysecho <<~MSG
      Releasing version #{current_version} to production.

      This will tag the current version and push it to the production branch.
    MSG
    sysecho "Are you ready to continue? (Press Enter to continue, Type 'x' and Enter to exit)".bg(:yellow).black
    input = $stdin.gets
    exit if input.chomp.match?(/^x/i)

    continue = syscall(
      ["git checkout #{working_branch}"],
      ["git branch -D #{staging_branch} 2>/dev/null || true"],
      ["git branch -D #{production_branch} 2>/dev/null || true"],
      ["git fetch origin #{staging_branch}:#{staging_branch} #{production_branch}:#{production_branch}"],
      ["git checkout #{production_branch}"],
      ["git rebase #{staging_branch}"],
      ["git tag -a v#{current_version} -m 'Release #{current_version}'"],
      ["git push origin #{production_branch}:#{production_branch} v#{current_version}:v#{current_version}"],
      ["git push origin v#{current_version}"]
    ) do
      tasker["#{name}:slack"].invoke("Released #{app_name} #{current_version} to production.", release_message_channel, ":chipmunk:")
      syscall ["git checkout #{working_branch}"]
    end

    abort "Release failed." unless continue

    sysecho <<~MSG
      Version #{current_version} released to production.

      Preparing to bump the version for the next release.

    MSG

    tasker["reissue"].invoke
    new_version = Object.const_get(version_constant)
    new_version_branch = "bump/begin-#{new_version.tr(".", "-")}"
    continue = syscall(["git checkout -b #{new_version_branch}"])

    abort "Bump failed." unless continue

    pr_url = "#{pull_request_url}/compare/#{working_branch}...#{new_version_branch}?expand=1&title=Begin%20#{current_version}"

    syscall(["git push origin #{new_version_branch} --force"]) do
      sysecho <<~MSG
        Branch #{new_version_branch} created.

        Open a PR to #{working_branch} to mark the version and update the chaneglog
        for the next release.

        Opening PR: #{pr_url}
      MSG
    end.then do |success|
      syscall ["open #{pr_url}"] if success
    end
  end

  namespace name do
    desc "Echo the configuration settings."
    task :config do
      sysecho "-- Discharger Configuration --".bg(:green).black
      sysecho "SHA: #{commit_identifier.call}".bg(:red).black
      instance_variables.sort.each do |var|
        value = instance_variable_get(var)
        value = value.call if value.is_a?(Proc) && value.arity.zero?
        sysecho "#{var.to_s.sub("@", "").ljust(24)}: #{value}".bg(:yellow).black
      end
      sysecho "----------------------------------".bg(:green).black
    end

    desc description
    task build: :environment do
      syscall(
        ["git fetch origin #{working_branch}"],
        ["git checkout #{working_branch}"],
        ["git branch -D #{staging_branch} 2>/dev/null || true"],
        ["git checkout -b #{staging_branch}"],
        ["git push origin #{staging_branch} --force"]
      ) do
        tasker["#{name}:slack"].invoke("Building #{app_name} #{commit_identifier.call} on #{staging_branch}.", release_message_channel)
        syscall ["git checkout #{working_branch}"]
      end
    end

    desc "Send a message to Slack."
    task :slack, [:text, :channel, :emoji] => :environment do |_, args|
      args.with_defaults(
        channel: release_message_channel,
        emoji: nil
      )
      client = Slack::Web::Client.new
      options = args.to_h
      options[:icon_emoji] = options.delete(:emoji) if options[:emoji]

      sysecho "Sending message to Slack:".bg(:green).black + " #{args[:text]}"
      result = client.chat_postMessage(**options)
      sysecho %(Message sent: #{result["ts"]})
    end

    desc <<~DESC
      ---------- STEP 1 ----------
      Prepare the current version for release to production (#{production_branch})

      This task will create a new branch to prepare the release. The CHANGELOG
      will be updated and the version will be bumped. The branch will be pushed
      to the remote repository.

      After the branch is created, open a PR to #{working_branch} to finalize
      the release.
    DESC
    task prepare: [:environment] do
      current_version = Object.const_get(version_constant)
      finish_branch = "bump/finish-#{current_version.tr(".", "-")}"

      syscall(
        ["git fetch origin #{working_branch}"],
        ["git checkout #{working_branch}"],
        ["git checkout -b #{finish_branch}"]
      )
      sysecho <<~MSG
        Branch #{finish_branch} created.

        Check the contents of the CHANGELOG and ensure that the text is correct.

        If you need to make changes, edit the CHANGELOG and save the file.
        Then return here to continue with this commit.
      MSG
      sysecho "Are you ready to continue? (Press Enter to continue, Type 'x' and Enter to exit)".bg(:yellow).black
      input = $stdin.gets
      exit if input.chomp.match?(/^x/i)

      tasker["reissue:finalize"].invoke

      params = {
        expand: 1,
        title: "Finish version #{current_version}",
        body: <<~BODY
          Completing development for #{current_version}.
        BODY
      }

      pr_url = "#{pull_request_url}/compare/#{finish_branch}?#{params.to_query}"

      continue = syscall ["git push origin #{finish_branch} --force"] do
        sysecho <<~MSG
          Branch #{finish_branch} created.
          Open a PR to #{working_branch} to finalize the release.

          #{pr_url}

          Once the PR is merged, pull down #{working_branch} and run
            'rake #{name}:stage'
          to stage the release branch.
        MSG
      end
      if continue
        syscall ["git checkout #{working_branch}"],
          ["open", pr_url]
      end
    end

    desc <<~DESC
      ---------- STEP 2 ----------
      Stage the release branch

      This task will update Stage, open a PR, and instruct you on the next steps.

      NOTE: If you just want to update the stage environment but aren't ready to release, run:

          bin/rails #{name}:build
    DESC
    task stage: [:environment] do
      tasker["build"].invoke
      current_version = Object.const_get(version_constant)

      params = {
        expand: 1,
        title: "Release #{current_version} to production",
        body: <<~BODY
          Deploy #{current_version} to production.
        BODY
      }

      pr_url = "#{pull_request_url}/compare/#{production_branch}...#{staging_branch}?#{params.to_query}"

      sysecho <<~MSG
        Branch #{staging_branch} updated.
        Open a PR to #{production_branch} to release the version.

        Opening PR: #{pr_url}

        Once the PR is **approved**, run 'rake release' to release the version.
      MSG
      syscall ["open #{pr_url}"]
    end
  end
end

#syscall(*steps, output: $stdout, error: $stderr) ⇒ Boolean

Run a multiple system commands and return true if all commands succeed If any command fails, the method will return false and stop executing any further commands.

Provide a block to evaluate the output of the command and return true if the command was successful. If the block returns false, the method will return false and stop executing any further commands.

Examples:

syscall(
  ["echo Hello, World!"],
  ["ls -l"]
)

Parameters:

  • *steps (Array<Array<String>>)

    an array of commands to run

  • block (Proc)

    a block to evaluate the output of the command

Returns:

  • (Boolean)

    true if all commands succeed, false otherwise



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/discharger/task.rb', line 76

def syscall(*steps, output: $stdout, error: $stderr)
  success = false
  stdout, stderr, status = nil
  steps.each do |cmd|
    puts cmd.join(" ").bg(:green).black
    stdout, stderr, status = Open3.capture3(*cmd)
    if status.success?
      output.puts stdout
      success = true
    else
      error.puts stderr
      success = false
      exit(status.exitstatus)
    end
  end
  if block_given?
    success = !!yield(stdout, stderr, status)
    # If the error reports that a rule was bypassed, consider the command successful
    # because we are bypassing the rule intentionally when merging the release branch
    # to the production branch.
    success = true if stderr.match?(/bypassed rule violations/i)
    abort(stderr) unless success
  end
  success
end

#sysecho(message, output: $stdout) ⇒ Object

Echo a message to the console

return [TrueClass]

Parameters:

  • message (String)

    the message to echo



106
107
108
109
# File 'lib/discharger/task.rb', line 106

def sysecho(message, output: $stdout)
  output.puts message
  true
end