discharger 0.3.5 → 0.4.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +29 -4
- data/README.md +58 -1
- data/lib/discharger/setup.rb +21 -16
- data/lib/discharger/setup_runner/command_factory.rb +45 -8
- data/lib/discharger/setup_runner/command_registry.rb +27 -0
- data/lib/discharger/setup_runner/commands/docker_command.rb +7 -1
- data/lib/discharger/setup_runner/commands/github_packages_command.rb +44 -10
- data/lib/discharger/setup_runner/runner.rb +25 -5
- data/lib/discharger/task.rb +184 -82
- data/lib/discharger/timing.rb +12 -0
- data/lib/discharger/version.rb +1 -1
- data/lib/generators/discharger/install/install_generator.rb +9 -0
- data/lib/generators/discharger/install/templates/setup +45 -24
- data/lib/generators/discharger/install/templates/setup.yml +7 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3212b79aa38f448d44523f2f83b622c7dfec1cff7bff08ed6033bbe1fea79761
|
|
4
|
+
data.tar.gz: 6d9d0c008a85560a666522c58a97ee08feae68ef1ea4e8f7cda86e29b0e8bd75
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a54728bf513a01a7f4f91507d0951f4075ab8d2fbd7a80cb01223bbd967f4f5bcc0dfba8a8842a3d3e6ab30c94995180bef978089c19a519ac07de608234f787
|
|
7
|
+
data.tar.gz: bf5d95387d7f861cfef6041871c39a946f986225a8c20aa3597b5dc3d5840e5f3ac16ebe5babc354da4b292e84820bcf8cc5a75b56d519414c12614e325a350b
|
data/CHANGELOG.md
CHANGED
|
@@ -5,14 +5,39 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
|
6
6
|
and this project adheres to [Semantic Versioning](http://semver.org/).
|
|
7
7
|
|
|
8
|
-
## [0.
|
|
8
|
+
## [0.4.1] - 2026-09-29
|
|
9
9
|
|
|
10
10
|
### Added
|
|
11
11
|
|
|
12
|
-
-
|
|
12
|
+
- Setup prints each step's elapsed time and the total (063521c)
|
|
13
|
+
- bin/setup opens with a do-not-edit notice, and `discharger:install --setup-only` regenerates only the script (71ee122)
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Gem author listed as Savannah Albanez (a48c8d8)
|
|
18
|
+
|
|
19
|
+
### Removed
|
|
20
|
+
|
|
21
|
+
- Stale Qualify setup script from the repo root, never shipped in the gem (8744c43)
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- Releases finish when the Slack token is missing or the post fails (3cf9b6d)
|
|
26
|
+
- bin/setup bootstraps a fresh clone: GitHub Packages credentials, bundle install, then re-exec under bundle exec (7196fe4)
|
|
27
|
+
- github_packages stores bundler credentials only after the source accepts them (41fab0f)
|
|
28
|
+
|
|
29
|
+
## [0.4.0] - 2026-09-24
|
|
15
30
|
|
|
16
31
|
### Added
|
|
17
32
|
|
|
18
|
-
-
|
|
33
|
+
- pr_label option opening the finalize and version-bump PRs via gh with that label (08e1d10)
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- Auto-deploy releases tag the newest commit that changed the version's changelog section instead of requiring it at HEAD, and no longer open a production PR (476fe1b)
|
|
38
|
+
- Auto-deploy releases merge the release tag into the production branch (a7a1b49)
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- retain_changelogs reaches Reissue, so finalize writes the archived changelog file (2a90edf)
|
|
43
|
+
- release:prepare branches from origin, so a stale local branch no longer yields an empty changelog (8eb5fd3)
|
data/README.md
CHANGED
|
@@ -187,6 +187,16 @@ end
|
|
|
187
187
|
|
|
188
188
|
This allows you to use environment variables to override the default branch names, or set project-specific defaults. The `DISCHARGER_BUILD_BRANCH` environment variable (shown above) provides a runtime override specifically for the build task.
|
|
189
189
|
|
|
190
|
+
### Auto-deploy Staging
|
|
191
|
+
|
|
192
|
+
For projects whose CI deploys the working branch to staging on every merge, set `task.auto_deploy_staging = true` and skip `rake release:stage`. Once the finalize PR merges, `rake release`:
|
|
193
|
+
|
|
194
|
+
1. Tags the newest commit that changed the current version's dated changelog section, so work merged after finalizing (or re-finalizing for a hotfix) is left out.
|
|
195
|
+
2. Pushes the tag. Production deploys from `v*` tags.
|
|
196
|
+
3. Merges the tag into `production_branch` so it tracks production. If that push is rejected, the release still completes with a warning.
|
|
197
|
+
|
|
198
|
+
Production ships the tagged commit, which can be older than what staging last ran.
|
|
199
|
+
|
|
190
200
|
## Development Setup Automation
|
|
191
201
|
|
|
192
202
|
Discharger includes a setup script that automates your development environment configuration. When you run the install generator, it creates a `bin/setup` script and a `config/setup.yml` configuration file.
|
|
@@ -201,6 +211,33 @@ $ bin/setup
|
|
|
201
211
|
|
|
202
212
|
This script is idempotent - you can run it multiple times safely, and it will ensure your environment is properly configured.
|
|
203
213
|
|
|
214
|
+
It works on a fresh clone with no gems installed. The generated script first runs a
|
|
215
|
+
standard-library-only pass that stores bundler credentials for a configured
|
|
216
|
+
`github_packages` source (from your GitHub CLI login, when its token has the
|
|
217
|
+
`read:packages` scope), installs the bundle, and then
|
|
218
|
+
re-execs itself under `bundle exec` so default gems such as psych never clash with
|
|
219
|
+
`Gemfile.lock`. The `DISCHARGER_SETUP_BUNDLED` environment variable marks the second
|
|
220
|
+
pass; the `github_packages` step later verifies the stored credentials and warns when
|
|
221
|
+
the token lacks the `read:packages` scope. Rails still has to boot before the steps
|
|
222
|
+
run, so an app whose boot needs `.env` or `config/database.yml` copies them in
|
|
223
|
+
`pre_steps` (see below).
|
|
224
|
+
|
|
225
|
+
Each step prints its elapsed time when it finishes, and the closing line reports the
|
|
226
|
+
total, so a slow setup shows which step to look at.
|
|
227
|
+
|
|
228
|
+
### Keeping bin/setup Generated
|
|
229
|
+
|
|
230
|
+
`bin/setup` is a verbatim copy of the gem's template and opens with a notice saying
|
|
231
|
+
so. Do not hand-edit it in an app: app-specific work belongs in `config/setup.yml`
|
|
232
|
+
(`pre_steps`, `steps`, `custom_steps`), and changes to the script itself belong in
|
|
233
|
+
this gem's template so every app picks them up. After bumping to a discharger
|
|
234
|
+
release that changed the template, regenerate the script without touching
|
|
235
|
+
`config/setup.yml` or the initializer:
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
$ bin/rails generate discharger:install --setup-only --force
|
|
239
|
+
```
|
|
240
|
+
|
|
204
241
|
### Configuration
|
|
205
242
|
|
|
206
243
|
The setup process is configured through `config/setup.yml`. Here's an example configuration:
|
|
@@ -295,7 +332,7 @@ steps:
|
|
|
295
332
|
- yarn
|
|
296
333
|
```
|
|
297
334
|
|
|
298
|
-
Leave the array empty or omit it entirely to run all available steps.
|
|
335
|
+
Leave the array empty or omit it entirely to run all available steps, in the order listed above. Custom commands registered through `Discharger::SetupRunner.register_command` run last.
|
|
299
336
|
|
|
300
337
|
### Adding Custom Commands
|
|
301
338
|
|
|
@@ -322,6 +359,26 @@ Each custom step can include:
|
|
|
322
359
|
- `description` - A description shown during setup
|
|
323
360
|
- `command` - The command to execute
|
|
324
361
|
- `condition` - An optional Ruby expression that must evaluate to true for the command to run
|
|
362
|
+
- `name` - An optional name that lets the step be scheduled in the `steps` list
|
|
363
|
+
|
|
364
|
+
Custom steps run after the built-in steps, in the order listed. To run one
|
|
365
|
+
earlier, give it a `name` and list that name in `steps`:
|
|
366
|
+
|
|
367
|
+
```yaml
|
|
368
|
+
custom_steps:
|
|
369
|
+
- name: import_certs
|
|
370
|
+
description: "Import development certificates"
|
|
371
|
+
command: "bin/import-certs"
|
|
372
|
+
|
|
373
|
+
steps:
|
|
374
|
+
- brew
|
|
375
|
+
- import_certs # runs between brew and bundler
|
|
376
|
+
- bundler
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
A named step listed in `steps` runs only at that position; named steps that
|
|
380
|
+
are not listed keep the default run-last behavior. A name that collides with
|
|
381
|
+
a built-in step resolves to the built-in, and a warning is logged.
|
|
325
382
|
|
|
326
383
|
### Creating Custom Command Classes
|
|
327
384
|
|
data/lib/discharger/setup.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
|
-
|
|
4
|
+
require_relative "timing"
|
|
5
5
|
|
|
6
6
|
# Unified entry point for Discharger setup.
|
|
7
7
|
# Handles both pre-Rails prerequisites and post-Rails setup commands.
|
|
@@ -34,21 +34,23 @@ module Discharger
|
|
|
34
34
|
validate_environment
|
|
35
35
|
print_header
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
elapsed = Timing.measure do
|
|
38
|
+
# Phase 1: Load bundler first (activates correct gem versions)
|
|
39
|
+
# This must happen before we parse YAML to avoid psych version conflicts
|
|
40
|
+
load_bundler
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
# Phase 2: Pre-Rails setup (env vars, system dependencies)
|
|
43
|
+
# These run AFTER bundler but BEFORE Rails loads
|
|
44
|
+
run_prerequisites
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
# Phase 3: Load Rails (uses env vars from phase 2)
|
|
47
|
+
load_rails
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
# Phase 4: Run Discharger commands (after Rails loads)
|
|
50
|
+
run_setup_commands
|
|
51
|
+
end
|
|
50
52
|
|
|
51
|
-
print_footer
|
|
53
|
+
print_footer(elapsed)
|
|
52
54
|
end
|
|
53
55
|
end
|
|
54
56
|
|
|
@@ -71,8 +73,8 @@ module Discharger
|
|
|
71
73
|
puts "Configuration loaded from: #{config_path}"
|
|
72
74
|
end
|
|
73
75
|
|
|
74
|
-
def print_footer
|
|
75
|
-
puts "\n== Setup completed successfully! =="
|
|
76
|
+
def print_footer(elapsed)
|
|
77
|
+
puts format("\n== Setup completed successfully! (%.2fs) ==", elapsed)
|
|
76
78
|
end
|
|
77
79
|
|
|
78
80
|
def load_bundler
|
|
@@ -89,8 +91,11 @@ module Discharger
|
|
|
89
91
|
# Load Rails from the standard location
|
|
90
92
|
rails_config = File.join(app_root, "config", "application.rb")
|
|
91
93
|
if File.exist?(rails_config)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
+
elapsed = Timing.measure do
|
|
95
|
+
require rails_config
|
|
96
|
+
Rails.application.initialize!
|
|
97
|
+
end
|
|
98
|
+
puts format("\n== Rails loaded (%.2fs) ==", elapsed)
|
|
94
99
|
else
|
|
95
100
|
puts "Warning: config/application.rb not found. Skipping Rails initialization."
|
|
96
101
|
end
|
|
@@ -25,32 +25,69 @@ module Discharger
|
|
|
25
25
|
|
|
26
26
|
def create_all_commands
|
|
27
27
|
commands = []
|
|
28
|
+
scheduled = []
|
|
28
29
|
|
|
29
|
-
# Create built-in commands from steps
|
|
30
|
+
# Create built-in and named custom commands from steps
|
|
30
31
|
if config.steps.any?
|
|
32
|
+
named_custom = named_custom_steps
|
|
31
33
|
config.steps.each do |step|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
name = step.to_s
|
|
35
|
+
if CommandRegistry.get(name)
|
|
36
|
+
if named_custom.key?(name)
|
|
37
|
+
logger&.warn "custom step name #{name.inspect} is shadowed by a built-in command; the built-in will run"
|
|
38
|
+
end
|
|
39
|
+
command = create_command(name)
|
|
40
|
+
commands << command if command
|
|
41
|
+
elsif (step_config = named_custom[name])
|
|
42
|
+
scheduled << step_config
|
|
43
|
+
commands << build_custom_command(step_config)
|
|
44
|
+
else
|
|
45
|
+
logger&.warn "unknown step #{name.inspect}; not a built-in or named custom step"
|
|
46
|
+
end
|
|
34
47
|
end
|
|
35
48
|
else
|
|
36
49
|
# If no steps specified, create all registered commands
|
|
37
|
-
CommandRegistry.
|
|
50
|
+
CommandRegistry.ordered_names.each do |name|
|
|
38
51
|
command = create_command(name)
|
|
39
52
|
commands << command if command
|
|
40
53
|
end
|
|
41
54
|
end
|
|
42
55
|
|
|
43
|
-
# Create custom commands
|
|
56
|
+
# Create the remaining custom commands
|
|
44
57
|
if config.respond_to?(:custom_steps) && config.custom_steps.any?
|
|
45
|
-
require_relative "commands/custom_command"
|
|
46
58
|
config.custom_steps.each do |step_config|
|
|
47
|
-
|
|
48
|
-
commands <<
|
|
59
|
+
next if scheduled.any? { |s| s.equal?(step_config) }
|
|
60
|
+
commands << build_custom_command(step_config)
|
|
49
61
|
end
|
|
50
62
|
end
|
|
51
63
|
|
|
52
64
|
commands
|
|
53
65
|
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
# Custom steps with a "name" key can be scheduled by that name in
|
|
70
|
+
# steps:. First entry wins a contested name; later duplicates keep the
|
|
71
|
+
# default run-last behavior.
|
|
72
|
+
def named_custom_steps
|
|
73
|
+
return {} unless config.respond_to?(:custom_steps)
|
|
74
|
+
|
|
75
|
+
config.custom_steps.each_with_object({}) do |step_config, named|
|
|
76
|
+
name = step_config["name"].to_s
|
|
77
|
+
next if name.empty?
|
|
78
|
+
|
|
79
|
+
if named.key?(name)
|
|
80
|
+
logger&.warn "duplicate custom step name #{name.inspect}; only the first entry can be scheduled"
|
|
81
|
+
next
|
|
82
|
+
end
|
|
83
|
+
named[name] = step_config
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def build_custom_command(step_config)
|
|
88
|
+
require_relative "commands/custom_command"
|
|
89
|
+
Commands::CustomCommand.new(config, app_root, logger, step_config)
|
|
90
|
+
end
|
|
54
91
|
end
|
|
55
92
|
end
|
|
56
93
|
end
|
|
@@ -3,6 +3,24 @@
|
|
|
3
3
|
module Discharger
|
|
4
4
|
module SetupRunner
|
|
5
5
|
class CommandRegistry
|
|
6
|
+
# The order built-in steps run in when setup.yml configures no steps.
|
|
7
|
+
# Registration order cannot be relied on here: it comes from enumerating
|
|
8
|
+
# Commands.constants, which shifts with require order. Credentials and
|
|
9
|
+
# toolchains have to be in place before bundler resolves gems.
|
|
10
|
+
DEFAULT_ORDER = %w[
|
|
11
|
+
brew
|
|
12
|
+
asdf
|
|
13
|
+
git
|
|
14
|
+
github_packages
|
|
15
|
+
bundler
|
|
16
|
+
yarn
|
|
17
|
+
config
|
|
18
|
+
docker
|
|
19
|
+
pg_tools
|
|
20
|
+
env
|
|
21
|
+
database
|
|
22
|
+
].freeze
|
|
23
|
+
|
|
6
24
|
class << self
|
|
7
25
|
def register(name, command_class)
|
|
8
26
|
commands[name.to_s] = command_class
|
|
@@ -20,6 +38,12 @@ module Discharger
|
|
|
20
38
|
commands.keys
|
|
21
39
|
end
|
|
22
40
|
|
|
41
|
+
# Registered names in pipeline order, with anything outside
|
|
42
|
+
# DEFAULT_ORDER (custom commands) appended in registration order.
|
|
43
|
+
def ordered_names
|
|
44
|
+
(DEFAULT_ORDER & names) + (names - DEFAULT_ORDER)
|
|
45
|
+
end
|
|
46
|
+
|
|
23
47
|
def clear
|
|
24
48
|
commands.clear
|
|
25
49
|
end
|
|
@@ -41,6 +65,9 @@ module Discharger
|
|
|
41
65
|
command_class = Commands.const_get(const_name)
|
|
42
66
|
next unless command_class < Commands::BaseCommand
|
|
43
67
|
next if command_class == Commands::BaseCommand
|
|
68
|
+
# Not schedulable by name: it takes a step_config argument and is
|
|
69
|
+
# instantiated by CommandFactory from custom_steps entries.
|
|
70
|
+
next if command_class == Commands::CustomCommand
|
|
44
71
|
|
|
45
72
|
# Convert class name to command name (e.g., AsdfCommand -> asdf)
|
|
46
73
|
command_name = const_name.to_s.sub(/Command$/, "").gsub(/([A-Z])/, '_\1').downcase.sub(/^_/, "")
|
|
@@ -104,12 +104,14 @@ module Discharger
|
|
|
104
104
|
log "Checking #{name} container"
|
|
105
105
|
|
|
106
106
|
if system_quiet("docker ps | grep #{name} > /dev/null 2>&1")
|
|
107
|
+
ensure_restart_policy(name)
|
|
107
108
|
log "#{name} container is already running"
|
|
108
109
|
return
|
|
109
110
|
end
|
|
110
111
|
|
|
111
112
|
# Check if container exists but is stopped
|
|
112
113
|
if system_quiet("docker inspect #{name} > /dev/null 2>&1")
|
|
114
|
+
ensure_restart_policy(name)
|
|
113
115
|
log "Starting existing #{name} container"
|
|
114
116
|
unless system_quiet("docker start #{name}")
|
|
115
117
|
log "Removing failed #{name} container"
|
|
@@ -148,7 +150,7 @@ module Discharger
|
|
|
148
150
|
def create_container(name:, port:, image:, internal_port:, env: {}, volume: nil)
|
|
149
151
|
log "Creating new #{name} container"
|
|
150
152
|
|
|
151
|
-
cmd = ["docker", "run", "-d", "--name", name, "-p", "#{port}:#{internal_port}"]
|
|
153
|
+
cmd = ["docker", "run", "-d", "--restart", "unless-stopped", "--name", name, "-p", "#{port}:#{internal_port}"]
|
|
152
154
|
env.each { |k, v| cmd.push("-e", "#{k}=#{v}") }
|
|
153
155
|
cmd.push("-v", volume) if volume
|
|
154
156
|
cmd.push(image)
|
|
@@ -156,6 +158,10 @@ module Discharger
|
|
|
156
158
|
system!(*cmd)
|
|
157
159
|
end
|
|
158
160
|
|
|
161
|
+
def ensure_restart_policy(name)
|
|
162
|
+
system!("docker", "update", "--restart", "unless-stopped", name)
|
|
163
|
+
end
|
|
164
|
+
|
|
159
165
|
def docker_available?
|
|
160
166
|
return true if system_quiet("which docker > /dev/null 2>&1")
|
|
161
167
|
|
|
@@ -6,10 +6,43 @@ module Discharger
|
|
|
6
6
|
module SetupRunner
|
|
7
7
|
module Commands
|
|
8
8
|
# Stores bundler credentials for a private GitHub Packages gem source
|
|
9
|
-
# using the GitHub CLI, so Gemfiles don't need embedded tokens.
|
|
10
|
-
#
|
|
11
|
-
#
|
|
9
|
+
# using the GitHub CLI, so Gemfiles don't need embedded tokens. A token
|
|
10
|
+
# the source rejects is not stored, so it cannot shadow a working
|
|
11
|
+
# credential in the user's own bundler config. Credentials are written
|
|
12
|
+
# to the app's .bundle/config — keep .bundle gitignored. Run this step
|
|
13
|
+
# before "bundler" in setup.yml.
|
|
12
14
|
class GithubPackagesCommand < BaseCommand
|
|
15
|
+
# A configured source with nothing scheduled to store its credentials
|
|
16
|
+
# is inert, and the only symptom is bundler failing against the
|
|
17
|
+
# private source. The Runner surfaces this before executing commands.
|
|
18
|
+
# Guarded for duck-typed configs passed to the public Runner API, and
|
|
19
|
+
# quiet when a custom step's command references the source (assumed
|
|
20
|
+
# to handle credentials itself).
|
|
21
|
+
def self.unscheduled_warning(config)
|
|
22
|
+
return unless config.respond_to?(:github_packages)
|
|
23
|
+
|
|
24
|
+
source = config.github_packages&.source
|
|
25
|
+
return if source.to_s.empty?
|
|
26
|
+
if config.respond_to?(:custom_steps) &&
|
|
27
|
+
config.custom_steps.any? { |step| step_handles_source?(step, source) }
|
|
28
|
+
return
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
"github_packages is configured but missing from steps; " \
|
|
32
|
+
"credentials will not be stored and bundler may fail for #{source}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# A referencing step only counts as handling credentials when it will
|
|
36
|
+
# actually run: CustomCommand skips itself when its condition is
|
|
37
|
+
# false, storing nothing on this run.
|
|
38
|
+
def self.step_handles_source?(step, source)
|
|
39
|
+
return false unless step["command"].to_s.include?(source)
|
|
40
|
+
|
|
41
|
+
require_relative "../condition_evaluator"
|
|
42
|
+
ConditionEvaluator.evaluate(step["condition"])
|
|
43
|
+
end
|
|
44
|
+
private_class_method :step_handles_source?
|
|
45
|
+
|
|
13
46
|
def execute
|
|
14
47
|
unless gh_installed?
|
|
15
48
|
log "GitHub CLI (gh) not found; skipping. Install gh and rerun setup or `bundle install` may fail for #{source}"
|
|
@@ -28,13 +61,14 @@ module Discharger
|
|
|
28
61
|
return
|
|
29
62
|
end
|
|
30
63
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
log "#{source} rejected the stored credentials. Your gh token may lack " \
|
|
36
|
-
"the read:packages scope — run `gh auth refresh -s read:packages` and rerun setup."
|
|
64
|
+
unless credentials_valid?(username, token)
|
|
65
|
+
log "#{source} rejected the gh token, so bundler credentials were left alone. The token " \
|
|
66
|
+
"may lack the read:packages scope — run `gh auth refresh -s read:packages` and rerun setup."
|
|
67
|
+
return
|
|
37
68
|
end
|
|
69
|
+
|
|
70
|
+
store_bundler_credentials(username, token)
|
|
71
|
+
log "Configured bundler credentials for #{source}"
|
|
38
72
|
end
|
|
39
73
|
|
|
40
74
|
def can_execute?
|
|
@@ -48,7 +82,7 @@ module Discharger
|
|
|
48
82
|
protected
|
|
49
83
|
|
|
50
84
|
def source
|
|
51
|
-
config.github_packages&.source
|
|
85
|
+
config.github_packages&.source
|
|
52
86
|
end
|
|
53
87
|
|
|
54
88
|
def gh_installed?
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
4
|
require "logger"
|
|
5
|
+
require_relative "../timing"
|
|
5
6
|
require_relative "command_factory"
|
|
6
7
|
|
|
7
8
|
module Discharger
|
|
@@ -15,7 +16,7 @@ module Discharger
|
|
|
15
16
|
@config = config
|
|
16
17
|
@app_root = app_root || Dir.pwd
|
|
17
18
|
@logger = logger || Logger.new($stdout)
|
|
18
|
-
@command_factory = CommandFactory.new(config, app_root, logger)
|
|
19
|
+
@command_factory = CommandFactory.new(config, @app_root, @logger)
|
|
19
20
|
end
|
|
20
21
|
|
|
21
22
|
def run
|
|
@@ -25,12 +26,16 @@ module Discharger
|
|
|
25
26
|
puts Rainbow("=" * 50).blue
|
|
26
27
|
end
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
warn_unscheduled_commands
|
|
30
|
+
|
|
31
|
+
elapsed = Timing.measure do
|
|
32
|
+
FileUtils.chdir app_root do
|
|
33
|
+
execute_commands
|
|
34
|
+
end
|
|
30
35
|
end
|
|
31
36
|
|
|
32
37
|
unless ENV["QUIET_SETUP"] || ENV["DISABLE_OUTPUT"]
|
|
33
|
-
puts Rainbow("\n✅ Setup completed successfully!").bright.green
|
|
38
|
+
puts Rainbow(format("\n✅ Setup completed successfully! (%.2fs)", elapsed)).bright.green
|
|
34
39
|
end
|
|
35
40
|
rescue => e
|
|
36
41
|
unless ENV["QUIET_SETUP"] || ENV["DISABLE_OUTPUT"]
|
|
@@ -76,6 +81,18 @@ module Discharger
|
|
|
76
81
|
@commands ||= command_factory.create_all_commands
|
|
77
82
|
end
|
|
78
83
|
|
|
84
|
+
# Checked at run time, after the customization block, so commands
|
|
85
|
+
# scheduled through add_command/insert_command_* count as scheduled.
|
|
86
|
+
def warn_unscheduled_commands
|
|
87
|
+
CommandRegistry.all.each do |klass|
|
|
88
|
+
next unless klass.respond_to?(:unscheduled_warning)
|
|
89
|
+
next if commands.any? { |command| command.is_a?(klass) }
|
|
90
|
+
|
|
91
|
+
message = klass.unscheduled_warning(config)
|
|
92
|
+
logger.warn message if message
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
79
96
|
def execute_commands
|
|
80
97
|
commands.each do |command|
|
|
81
98
|
execute_command(command)
|
|
@@ -94,7 +111,10 @@ module Discharger
|
|
|
94
111
|
unless ENV["QUIET_SETUP"] || ENV["DISABLE_OUTPUT"]
|
|
95
112
|
puts Rainbow("\n▶️ #{command.description}").bright
|
|
96
113
|
end
|
|
97
|
-
command.execute
|
|
114
|
+
elapsed = Timing.measure { command.execute }
|
|
115
|
+
unless ENV["QUIET_SETUP"] || ENV["DISABLE_OUTPUT"]
|
|
116
|
+
puts Rainbow(format(" %s: %.2fs", command.description, elapsed)).faint
|
|
117
|
+
end
|
|
98
118
|
rescue => e
|
|
99
119
|
unless ENV["QUIET_SETUP"] || ENV["DISABLE_OUTPUT"]
|
|
100
120
|
require "rainbow"
|
data/lib/discharger/task.rb
CHANGED
|
@@ -25,6 +25,7 @@ module Discharger
|
|
|
25
25
|
reissue.clear_fragments = task.clear_fragments
|
|
26
26
|
reissue.tag_pattern = task.tag_pattern
|
|
27
27
|
reissue.runbook_file = task.runbook_file
|
|
28
|
+
reissue.retain_changelogs = task.retain_changelogs
|
|
28
29
|
end
|
|
29
30
|
task.define
|
|
30
31
|
task
|
|
@@ -46,6 +47,7 @@ module Discharger
|
|
|
46
47
|
attr_accessor :app_name
|
|
47
48
|
attr_accessor :commit_identifier
|
|
48
49
|
attr_accessor :pull_request_url
|
|
50
|
+
attr_accessor :pr_label
|
|
49
51
|
attr_accessor :fragment_directory
|
|
50
52
|
attr_accessor :fragment
|
|
51
53
|
attr_accessor :clear_fragments
|
|
@@ -134,32 +136,41 @@ module Discharger
|
|
|
134
136
|
ERROR
|
|
135
137
|
end
|
|
136
138
|
|
|
137
|
-
#
|
|
138
|
-
def
|
|
139
|
-
|
|
140
|
-
|
|
139
|
+
# The newest commit that changed the current version's dated changelog section
|
|
140
|
+
def find_release_commit!(branch, output: $stdout)
|
|
141
|
+
version = Object.const_get(version_constant)
|
|
142
|
+
section = /^## \[#{Regexp.escape(version)}\] - \d{4}-\d{2}-\d{2}.*?(?=^## \[|\z)/m
|
|
143
|
+
sha = git_file_commits(branch, changelog_file).find do |candidate_sha|
|
|
144
|
+
released = git_show_at_commit(candidate_sha, changelog_file)&.[](section)
|
|
145
|
+
released && released != git_show_at_commit("#{candidate_sha}^", changelog_file)&.[](section)
|
|
146
|
+
end
|
|
141
147
|
|
|
142
|
-
if
|
|
148
|
+
if sha.nil?
|
|
143
149
|
abort <<~ERROR.bg(:red).white
|
|
144
|
-
Could not
|
|
145
|
-
|
|
146
|
-
HEAD: #{head_sha || "not found"}
|
|
147
|
-
Release commit: #{release_sha || "not found"}
|
|
148
|
-
|
|
149
|
-
Ensure #{branch} exists and #{version_file} has been modified.
|
|
150
|
+
Could not locate a release commit.
|
|
151
|
+
Ensure #{branch} exists and #{changelog_file} has been finalized for #{version}.
|
|
150
152
|
ERROR
|
|
151
153
|
end
|
|
152
154
|
|
|
153
|
-
|
|
155
|
+
sysecho("✓ Release commit: #{sha[0, 8]}".bg(:green).black, output:)
|
|
156
|
+
sha
|
|
157
|
+
end
|
|
154
158
|
|
|
155
|
-
|
|
156
|
-
|
|
159
|
+
def merge_tag_into_production_branch(tag, output: $stdout)
|
|
160
|
+
git = ->(*args) do
|
|
161
|
+
stdout, stderr, status = Open3.capture3("git", *args)
|
|
162
|
+
return stdout.strip if status.success?
|
|
157
163
|
|
|
158
|
-
|
|
159
|
-
|
|
164
|
+
sysecho("Could not merge #{tag} into #{production_branch}: #{stderr.strip}".bg(:yellow).black, output:)
|
|
165
|
+
nil
|
|
166
|
+
end
|
|
160
167
|
|
|
161
|
-
|
|
162
|
-
|
|
168
|
+
return false unless git.call("fetch", "origin", production_branch)
|
|
169
|
+
return false unless (tree = git.call("merge-tree", "--write-tree", "origin/#{production_branch}", tag))
|
|
170
|
+
return false unless (commit = git.call("commit-tree", tree, "-p", "origin/#{production_branch}", "-p", "#{tag}^{commit}", "-m", "Merge tag '#{tag}'"))
|
|
171
|
+
return false unless git.call("push", "origin", "#{commit}:refs/heads/#{production_branch}")
|
|
172
|
+
|
|
173
|
+
sysecho("✓ Merged #{tag} into #{production_branch}".bg(:green).black, output:)
|
|
163
174
|
end
|
|
164
175
|
|
|
165
176
|
def git_show_version(branch)
|
|
@@ -168,16 +179,57 @@ module Discharger
|
|
|
168
179
|
content[/VERSION\s*=\s*["']([^"']+)["']/, 1]
|
|
169
180
|
end
|
|
170
181
|
|
|
171
|
-
def
|
|
172
|
-
stdout, _, status = Open3.capture3("git", "
|
|
173
|
-
return
|
|
174
|
-
stdout.strip
|
|
182
|
+
def git_file_commits(branch, path)
|
|
183
|
+
stdout, _, status = Open3.capture3("git", "log", "--no-merges", branch, "--format=%H", "--", path)
|
|
184
|
+
return [] unless status.success?
|
|
185
|
+
stdout.lines.map(&:strip)
|
|
175
186
|
end
|
|
176
187
|
|
|
177
|
-
def
|
|
178
|
-
stdout, _, status = Open3.capture3("git", "
|
|
188
|
+
def git_show_at_commit(sha, path)
|
|
189
|
+
stdout, _, status = Open3.capture3("git", "show", "#{sha}:#{path}")
|
|
179
190
|
return nil unless status.success?
|
|
180
|
-
stdout
|
|
191
|
+
stdout
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def ensure_clean_worktree!
|
|
195
|
+
stdout, _, status = Open3.capture3("git", "status", "--porcelain")
|
|
196
|
+
return true if status.success? && stdout.empty?
|
|
197
|
+
|
|
198
|
+
abort "Working tree has uncommitted changes. Commit or stash them before running rake #{name}:prepare."
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def validate_pr_label!
|
|
202
|
+
return true unless pr_label
|
|
203
|
+
|
|
204
|
+
stdout, stderr, status = Open3.capture3("gh", "label", "list", "--search", pr_label, "--json", "name", "--jq", ".[].name")
|
|
205
|
+
return true if status.success? && stdout.lines.map(&:chomp).include?(pr_label)
|
|
206
|
+
|
|
207
|
+
abort <<~ERROR.bg(:red).white
|
|
208
|
+
Could not find GitHub label '#{pr_label}'.
|
|
209
|
+
|
|
210
|
+
#{stderr}
|
|
211
|
+
ERROR
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def create_labeled_pr!(head:, title:, body:)
|
|
215
|
+
if (pr_number = existing_pr_number(working_branch, head))
|
|
216
|
+
return sysecho("Reusing existing PR ##{pr_number} for #{head}.")
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
syscall(
|
|
220
|
+
["gh", "pr", "create", "--base", working_branch, "--head", head, "--title", title, "--body", body, "--label", pr_label]
|
|
221
|
+
)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def ensure_branch_not_ahead!(branch)
|
|
225
|
+
stdout, _, status = Open3.capture3("git", "rev-list", "--count", "origin/#{branch}..#{branch}")
|
|
226
|
+
return true if status.success? && stdout.strip == "0"
|
|
227
|
+
|
|
228
|
+
abort <<~ERROR.bg(:red).white
|
|
229
|
+
Local #{branch} has commits not on origin/#{branch}. Refusing to reset --hard.
|
|
230
|
+
|
|
231
|
+
Push or remove them before retrying.
|
|
232
|
+
ERROR
|
|
181
233
|
end
|
|
182
234
|
|
|
183
235
|
# The post-release steps collected from "Runbook:" commit trailers.
|
|
@@ -214,6 +266,10 @@ module Discharger
|
|
|
214
266
|
message
|
|
215
267
|
end
|
|
216
268
|
|
|
269
|
+
def echo_unsent_slack_message(text, channel)
|
|
270
|
+
sysecho "Post this to #{channel} yourself:\n\n#{text}\n"
|
|
271
|
+
end
|
|
272
|
+
|
|
217
273
|
def existing_pr_number(base, head)
|
|
218
274
|
stdout, _, status = Open3.capture3(
|
|
219
275
|
"gh", "pr", "list",
|
|
@@ -258,6 +314,7 @@ module Discharger
|
|
|
258
314
|
unless system("gh --version > /dev/null 2>&1")
|
|
259
315
|
abort "Error: GitHub CLI (gh) is required for the release process but was not found. Install it: https://cli.github.com"
|
|
260
316
|
end
|
|
317
|
+
validate_pr_label!
|
|
261
318
|
|
|
262
319
|
current_version = Object.const_get(version_constant)
|
|
263
320
|
|
|
@@ -265,10 +322,16 @@ module Discharger
|
|
|
265
322
|
# instead of staging_branch (for CI/CD pipelines that auto-deploy staging)
|
|
266
323
|
release_source = auto_deploy_staging ? working_branch : staging_branch
|
|
267
324
|
|
|
325
|
+
release_action = if auto_deploy_staging
|
|
326
|
+
"This will tag the release commit on #{working_branch}, push the tag, and merge it into #{production_branch}."
|
|
327
|
+
else
|
|
328
|
+
"This will tag the current version and push it to the production branch."
|
|
329
|
+
end
|
|
330
|
+
|
|
268
331
|
sysecho <<~MSG
|
|
269
332
|
Releasing version #{current_version} to production.
|
|
270
333
|
|
|
271
|
-
|
|
334
|
+
#{release_action}
|
|
272
335
|
Release source: #{release_source}
|
|
273
336
|
MSG
|
|
274
337
|
sysecho "Are you ready to continue? (Press Enter to continue, Type 'x' and Enter to exit)".bg(:yellow).black
|
|
@@ -283,48 +346,39 @@ module Discharger
|
|
|
283
346
|
|
|
284
347
|
if auto_deploy_staging
|
|
285
348
|
syscall(
|
|
286
|
-
["git fetch origin #{
|
|
349
|
+
["git fetch origin #{working_branch}"],
|
|
287
350
|
["git reset --hard origin/#{working_branch}"]
|
|
288
351
|
)
|
|
289
|
-
|
|
352
|
+
tag_ref = find_release_commit!(release_source)
|
|
290
353
|
else
|
|
291
354
|
syscall(
|
|
292
355
|
["git fetch origin #{release_source}:#{release_source} #{production_branch}:#{production_branch}"]
|
|
293
356
|
)
|
|
294
357
|
validate_version_match!(staging_branch, working_branch)
|
|
295
|
-
end
|
|
296
358
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
if pr_number
|
|
301
|
-
sysecho "Reusing existing PR ##{pr_number} from #{release_source} to #{production_branch}."
|
|
302
|
-
pr_ref = pr_number
|
|
359
|
+
pr_ref = release_source
|
|
360
|
+
if pr_already_merged?(pr_ref)
|
|
361
|
+
sysecho "PR #{pr_ref} is already merged. Continuing..."
|
|
303
362
|
else
|
|
304
363
|
syscall(
|
|
305
|
-
["gh pr
|
|
364
|
+
["gh pr merge #{pr_ref} --merge"]
|
|
306
365
|
)
|
|
307
366
|
end
|
|
308
|
-
end
|
|
309
367
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
else
|
|
313
|
-
syscall(
|
|
314
|
-
["gh pr merge #{pr_ref} --merge"]
|
|
315
|
-
)
|
|
368
|
+
syscall(["git fetch origin #{production_branch}:#{production_branch}"])
|
|
369
|
+
tag_ref = production_branch
|
|
316
370
|
end
|
|
317
371
|
|
|
372
|
+
slack_sha = auto_deploy_staging ? tag_ref[0, 8] : commit_identifier.call
|
|
318
373
|
continue = syscall(
|
|
319
|
-
["git
|
|
320
|
-
["git tag -a v#{current_version} -m 'Release #{current_version}' #{production_branch}"],
|
|
374
|
+
["git tag -a v#{current_version} -m 'Release #{current_version}' #{tag_ref}"],
|
|
321
375
|
["git push origin v#{current_version}"]
|
|
322
376
|
) do
|
|
323
|
-
tasker["#{name}:slack"].invoke("Released #{app_name} #{current_version} (#{
|
|
377
|
+
tasker["#{name}:slack"].invoke("Released #{app_name} #{current_version} (#{slack_sha}) to production.", release_message_channel, ":chipmunk:")
|
|
324
378
|
# Capture the root before replying; each post overwrites last_message_ts.
|
|
325
379
|
thread_ts = last_message_ts
|
|
326
380
|
if thread_ts.present?
|
|
327
|
-
text = File.read(Rails.root.join(changelog_file))
|
|
381
|
+
text = git_show_at_commit(tag_ref, changelog_file) || File.read(Rails.root.join(changelog_file))
|
|
328
382
|
tasker["#{name}:slack"].reenable
|
|
329
383
|
tasker["#{name}:slack"].invoke(text, release_message_channel, ":log:", thread_ts)
|
|
330
384
|
|
|
@@ -335,6 +389,7 @@ module Discharger
|
|
|
335
389
|
end
|
|
336
390
|
|
|
337
391
|
abort "Release failed." unless continue
|
|
392
|
+
merge_tag_into_production_branch("v#{current_version}") if auto_deploy_staging
|
|
338
393
|
|
|
339
394
|
sysecho <<~MSG
|
|
340
395
|
Version #{current_version} released to production.
|
|
@@ -346,20 +401,26 @@ module Discharger
|
|
|
346
401
|
|
|
347
402
|
new_version_branch = `git rev-parse --abbrev-ref HEAD`.strip
|
|
348
403
|
new_version = new_version_branch.split("/").last
|
|
349
|
-
params = {expand: 1, title: "Bump version to #{new_version}"}
|
|
350
|
-
pr_url = "#{pull_request_url}/compare/#{working_branch}...#{new_version_branch}?#{params.to_query}"
|
|
351
404
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
405
|
+
if pr_label
|
|
406
|
+
syscall(["git push origin #{new_version_branch} --force"])
|
|
407
|
+
create_labeled_pr!(head: new_version_branch, title: "Bump version to #{new_version}", body: "")
|
|
408
|
+
else
|
|
409
|
+
params = {expand: 1, title: "Bump version to #{new_version}"}
|
|
410
|
+
pr_url = "#{pull_request_url}/compare/#{working_branch}...#{new_version_branch}?#{params.to_query}"
|
|
411
|
+
|
|
412
|
+
syscall(["git push origin #{new_version_branch} --force"]) do
|
|
413
|
+
sysecho <<~MSG
|
|
414
|
+
Branch #{new_version_branch} created.
|
|
355
415
|
|
|
356
|
-
|
|
357
|
-
|
|
416
|
+
Open a PR to #{working_branch} to mark the version and update the chaneglog
|
|
417
|
+
for the next release.
|
|
358
418
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
419
|
+
Opening PR: #{pr_url}
|
|
420
|
+
MSG
|
|
421
|
+
end.then do |success|
|
|
422
|
+
syscall ["open", pr_url] if success
|
|
423
|
+
end
|
|
363
424
|
end
|
|
364
425
|
end
|
|
365
426
|
|
|
@@ -402,17 +463,33 @@ module Discharger
|
|
|
402
463
|
|
|
403
464
|
desc "Send a message to Slack."
|
|
404
465
|
task :slack, [:text, :channel, :emoji, :ts] => :environment do |_, args|
|
|
466
|
+
instance_variable_set(:@last_message_ts, nil)
|
|
405
467
|
args.with_defaults(
|
|
406
468
|
channel: release_message_channel,
|
|
407
469
|
emoji: nil
|
|
408
470
|
)
|
|
471
|
+
if chat_token.blank?
|
|
472
|
+
sysecho <<~MSG.bg(:yellow).black
|
|
473
|
+
Slack message not sent: chat_token is not set.
|
|
474
|
+
Set it with the Slack release token from your team's password manager before the next release.
|
|
475
|
+
MSG
|
|
476
|
+
echo_unsent_slack_message(args[:text], args[:channel])
|
|
477
|
+
next
|
|
478
|
+
end
|
|
479
|
+
|
|
409
480
|
client = Slack::Web::Client.new
|
|
410
481
|
options = args.to_h
|
|
411
482
|
options[:icon_emoji] = options.delete(:emoji) if options[:emoji]
|
|
412
483
|
options[:thread_ts] = options.delete(:ts) if options[:ts]
|
|
413
484
|
|
|
414
485
|
sysecho "Sending message to Slack:".bg(:green).black + " #{args[:text]}"
|
|
415
|
-
|
|
486
|
+
begin
|
|
487
|
+
result = client.chat_postMessage(**options)
|
|
488
|
+
rescue Faraday::Error => e
|
|
489
|
+
sysecho "Slack message not sent: #{e.message}.".bg(:yellow).black
|
|
490
|
+
echo_unsent_slack_message(args[:text], args[:channel])
|
|
491
|
+
next
|
|
492
|
+
end
|
|
416
493
|
instance_variable_set(:@last_message_ts, result["ts"])
|
|
417
494
|
sysecho %(Message sent: #{result["ts"]})
|
|
418
495
|
end
|
|
@@ -429,12 +506,19 @@ module Discharger
|
|
|
429
506
|
the release.
|
|
430
507
|
DESC
|
|
431
508
|
task prepare: [:environment] do
|
|
509
|
+
ensure_clean_worktree!
|
|
510
|
+
validate_pr_label!
|
|
511
|
+
|
|
432
512
|
current_version = Object.const_get(version_constant)
|
|
433
513
|
finish_branch = "bump/finish-#{current_version.tr(".", "-")}"
|
|
434
514
|
|
|
435
515
|
syscall(
|
|
436
516
|
["git fetch origin #{working_branch}"],
|
|
437
|
-
["git checkout #{working_branch}"]
|
|
517
|
+
["git checkout #{working_branch}"]
|
|
518
|
+
)
|
|
519
|
+
ensure_branch_not_ahead!(working_branch)
|
|
520
|
+
syscall(
|
|
521
|
+
["git reset --hard origin/#{working_branch}"],
|
|
438
522
|
["git checkout -b #{finish_branch}"]
|
|
439
523
|
)
|
|
440
524
|
sysecho <<~MSG
|
|
@@ -451,34 +535,52 @@ module Discharger
|
|
|
451
535
|
|
|
452
536
|
tasker["reissue:finalize"].invoke
|
|
453
537
|
|
|
454
|
-
params = {
|
|
455
|
-
expand: 1,
|
|
456
|
-
title: "Finish version #{current_version}",
|
|
457
|
-
body: <<~BODY
|
|
458
|
-
Completing development for #{current_version}.
|
|
459
|
-
BODY
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
pr_url = "#{pull_request_url}/compare/#{finish_branch}?#{params.to_query}"
|
|
463
|
-
|
|
464
538
|
next_step = auto_deploy_staging ? "rake #{name}" : "rake #{name}:stage"
|
|
465
539
|
next_step_desc = auto_deploy_staging ? "release to production" : "stage the release branch"
|
|
540
|
+
after_merge = <<~MSG.chomp
|
|
541
|
+
Once the PR is merged, pull down #{working_branch} and run
|
|
542
|
+
'#{next_step}'
|
|
543
|
+
to #{next_step_desc}.
|
|
544
|
+
MSG
|
|
466
545
|
|
|
467
|
-
|
|
546
|
+
if pr_label
|
|
547
|
+
syscall ["git push origin #{finish_branch} --force"]
|
|
548
|
+
create_labeled_pr!(
|
|
549
|
+
head: finish_branch,
|
|
550
|
+
title: "Finish version #{current_version}",
|
|
551
|
+
body: "Completing development for #{current_version}."
|
|
552
|
+
)
|
|
468
553
|
sysecho <<~MSG
|
|
469
|
-
Branch #{finish_branch}
|
|
470
|
-
Open a PR to #{working_branch} to finalize the release.
|
|
471
|
-
|
|
472
|
-
#{pr_url}
|
|
554
|
+
Branch #{finish_branch} pushed and its PR labeled '#{pr_label}'.
|
|
473
555
|
|
|
474
|
-
|
|
475
|
-
'#{next_step}'
|
|
476
|
-
to #{next_step_desc}.
|
|
556
|
+
#{after_merge}
|
|
477
557
|
MSG
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
558
|
+
syscall ["git checkout #{working_branch}"]
|
|
559
|
+
else
|
|
560
|
+
params = {
|
|
561
|
+
expand: 1,
|
|
562
|
+
title: "Finish version #{current_version}",
|
|
563
|
+
body: <<~BODY
|
|
564
|
+
Completing development for #{current_version}.
|
|
565
|
+
BODY
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
pr_url = "#{pull_request_url}/compare/#{finish_branch}?#{params.to_query}"
|
|
569
|
+
|
|
570
|
+
continue = syscall ["git push origin #{finish_branch} --force"] do
|
|
571
|
+
sysecho <<~MSG
|
|
572
|
+
Branch #{finish_branch} created.
|
|
573
|
+
Open a PR to #{working_branch} to finalize the release.
|
|
574
|
+
|
|
575
|
+
#{pr_url}
|
|
576
|
+
|
|
577
|
+
#{after_merge}
|
|
578
|
+
MSG
|
|
579
|
+
end
|
|
580
|
+
if continue
|
|
581
|
+
syscall ["git checkout #{working_branch}"],
|
|
582
|
+
["open", pr_url]
|
|
583
|
+
end
|
|
482
584
|
end
|
|
483
585
|
end
|
|
484
586
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Discharger
|
|
4
|
+
module Timing
|
|
5
|
+
# Seconds the block took, from the monotonic clock.
|
|
6
|
+
def self.measure
|
|
7
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
8
|
+
yield
|
|
9
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
data/lib/discharger/version.rb
CHANGED
|
@@ -8,7 +8,14 @@ module Discharger
|
|
|
8
8
|
default: "bin/setup",
|
|
9
9
|
desc: "Path where the setup script should be created"
|
|
10
10
|
|
|
11
|
+
class_option :setup_only,
|
|
12
|
+
type: :boolean,
|
|
13
|
+
default: false,
|
|
14
|
+
desc: "Only (re)generate the setup script, leaving config/setup.yml and the initializer alone"
|
|
15
|
+
|
|
11
16
|
def copy_initializer
|
|
17
|
+
return if options[:setup_only]
|
|
18
|
+
|
|
12
19
|
template "discharger_initializer.rb", "config/initializers/discharger.rb"
|
|
13
20
|
end
|
|
14
21
|
|
|
@@ -18,6 +25,8 @@ module Discharger
|
|
|
18
25
|
end
|
|
19
26
|
|
|
20
27
|
def create_sample_setup_yml
|
|
28
|
+
return if options[:setup_only]
|
|
29
|
+
|
|
21
30
|
template "setup.yml", "config/setup.yml"
|
|
22
31
|
end
|
|
23
32
|
end
|
|
@@ -1,30 +1,51 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
|
|
3
|
-
#
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
begin
|
|
20
|
-
|
|
21
|
-
rescue
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
3
|
+
# GENERATED BY DISCHARGER. DO NOT EDIT. Edits here are lost on regeneration.
|
|
4
|
+
#
|
|
5
|
+
# App setup belongs in config/setup.yml (pre_steps, steps, custom_steps).
|
|
6
|
+
# Script changes belong in the discharger gem's template,
|
|
7
|
+
# lib/generators/discharger/install/templates/setup; release it, bump it, then
|
|
8
|
+
# bin/rails generate discharger:install --setup-only --force
|
|
9
|
+
|
|
10
|
+
script = File.expand_path(__FILE__)
|
|
11
|
+
Dir.chdir(File.expand_path("..", __dir__))
|
|
12
|
+
|
|
13
|
+
# First pass, standard library only: the bundle may not exist yet. Re-exec
|
|
14
|
+
# under bundle exec before yaml loads, or a default psych clashes with Gemfile.lock.
|
|
15
|
+
# Mirrors the github_packages and bundler steps, which re-run under bundle exec.
|
|
16
|
+
unless ENV["DISCHARGER_SETUP_BUNDLED"]
|
|
17
|
+
def store_github_packages_credentials
|
|
18
|
+
require "yaml"
|
|
19
|
+
source = begin
|
|
20
|
+
YAML.safe_load_file("config/setup.yml")&.dig("github_packages", "source")
|
|
21
|
+
rescue SystemCallError, Psych::Exception
|
|
22
|
+
nil # reported by Discharger::Setup on the second pass
|
|
23
|
+
end
|
|
24
|
+
return if source.to_s.empty?
|
|
25
|
+
unless `gh auth status 2>&1`.include?("read:packages")
|
|
26
|
+
warn "Skipping #{source} credentials: the gh token lacks read:packages (gh auth refresh -s read:packages)"
|
|
27
|
+
return
|
|
25
28
|
end
|
|
29
|
+
|
|
30
|
+
username = `gh api user --jq .login 2>/dev/null`.strip
|
|
31
|
+
token = `gh auth token 2>/dev/null`.strip
|
|
32
|
+
return if username.empty? || token.empty?
|
|
33
|
+
|
|
34
|
+
system("bundle", "config", "set", "--local", source, "#{username}:#{token}", out: File::NULL, err: File::NULL)
|
|
26
35
|
end
|
|
27
36
|
|
|
28
|
-
|
|
29
|
-
|
|
37
|
+
unless system("bundle", "check", out: File::NULL, err: File::NULL)
|
|
38
|
+
store_github_packages_credentials
|
|
39
|
+
system("bundle", "install") || abort(<<~MSG)
|
|
40
|
+
|
|
41
|
+
== bundle install failed ==
|
|
42
|
+
If the Gemfile uses a private GitHub Packages source, run `gh auth login`
|
|
43
|
+
(or `gh auth refresh -s read:packages`) and rerun bin/setup.
|
|
44
|
+
MSG
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
exec({"DISCHARGER_SETUP_BUNDLED" => "1"}, "bundle", "exec", RbConfig.ruby, script, *ARGV)
|
|
30
48
|
end
|
|
49
|
+
|
|
50
|
+
require "discharger/setup"
|
|
51
|
+
Discharger::Setup.run("config/setup.yml")
|
|
@@ -49,6 +49,8 @@ steps:
|
|
|
49
49
|
- brew
|
|
50
50
|
- asdf
|
|
51
51
|
- git
|
|
52
|
+
# Uncomment with the github_packages block above. Must precede bundler.
|
|
53
|
+
# - github_packages
|
|
52
54
|
- bundler
|
|
53
55
|
- yarn
|
|
54
56
|
- config
|
|
@@ -60,6 +62,11 @@ steps:
|
|
|
60
62
|
# - pg_tools
|
|
61
63
|
|
|
62
64
|
# Custom commands for application-specific setup (run AFTER Rails loads)
|
|
65
|
+
# Steps run last by default. Give a step a name and list it in steps: above
|
|
66
|
+
# to run it earlier, e.g.:
|
|
67
|
+
# - name: import_certs
|
|
68
|
+
# description: "Import development certificates"
|
|
69
|
+
# command: "bin/import-certs"
|
|
63
70
|
custom_steps:
|
|
64
71
|
- description: "Seed application data"
|
|
65
72
|
command: "bin/rails db:seed"
|
metadata
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: discharger
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jim Gay
|
|
8
|
-
- Savannah
|
|
8
|
+
- Savannah Albanez
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
11
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
@@ -135,6 +135,7 @@ files:
|
|
|
135
135
|
- lib/discharger/setup_runner/runner.rb
|
|
136
136
|
- lib/discharger/setup_runner/version.rb
|
|
137
137
|
- lib/discharger/task.rb
|
|
138
|
+
- lib/discharger/timing.rb
|
|
138
139
|
- lib/discharger/version.rb
|
|
139
140
|
- lib/generators/discharger/install/install_generator.rb
|
|
140
141
|
- lib/generators/discharger/install/templates/discharger_initializer.rb
|