dev_onboarder 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +49 -0
- data/LICENSE.txt +21 -0
- data/README.md +127 -0
- data/Rakefile +14 -0
- data/app/controllers/dev_onboarder/setup_controller.rb +15 -0
- data/app/views/dev_onboarder/setup/show.html.erb +61 -0
- data/config/routes.rb +5 -0
- data/exe/dev_onboarder +6 -0
- data/lib/dev_onboarder/cli.rb +119 -0
- data/lib/dev_onboarder/engine.rb +46 -0
- data/lib/dev_onboarder/env_file.rb +25 -0
- data/lib/dev_onboarder/lines.rb +42 -0
- data/lib/dev_onboarder/machine_tools.rb +43 -0
- data/lib/dev_onboarder/overview.rb +54 -0
- data/lib/dev_onboarder/record.rb +51 -0
- data/lib/dev_onboarder/record_location.rb +17 -0
- data/lib/dev_onboarder/requirement.rb +22 -0
- data/lib/dev_onboarder/requirements.rb +67 -0
- data/lib/dev_onboarder/secret_prompt.rb +40 -0
- data/lib/dev_onboarder/secrets.rb +16 -0
- data/lib/dev_onboarder/setup.rb +67 -0
- data/lib/dev_onboarder/shell.rb +24 -0
- data/lib/dev_onboarder/status.rb +33 -0
- data/lib/dev_onboarder/version.rb +5 -0
- data/lib/dev_onboarder.rb +9 -0
- data/the_local/agents/dev_onboarder-develop.md +116 -0
- data/the_local/agents/dev_onboarder-info.md +43 -0
- data/the_local/agents/dev_onboarder-install.md +47 -0
- data/the_local/interface.yml +36 -0
- metadata +77 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module DevOnboarder
|
|
7
|
+
Result = Data.define(:met, :checked_at, :fingerprint) do
|
|
8
|
+
def initialize(met:, checked_at:, fingerprint: nil)
|
|
9
|
+
super
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
class Record
|
|
14
|
+
def initialize(path)
|
|
15
|
+
@path = path
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def save(results)
|
|
19
|
+
saved = results.to_h { |key, result| [key.to_s, stored(result)] }
|
|
20
|
+
File.write(@path, JSON.pretty_generate("results" => stored_results.merge(saved)))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def result_for(key)
|
|
24
|
+
stored = stored_results[key.to_s]
|
|
25
|
+
stored && Result.new(met: stored.fetch("met"), checked_at: Time.iso8601(stored.fetch("checked_at")),
|
|
26
|
+
fingerprint: stored["fingerprint"])
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def unreadable?
|
|
30
|
+
File.exist?(@path) && parsed.nil?
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def stored_results
|
|
36
|
+
parsed || {}
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def parsed
|
|
40
|
+
return unless File.exist?(@path)
|
|
41
|
+
|
|
42
|
+
JSON.parse(File.read(@path)).fetch("results")
|
|
43
|
+
rescue JSON::ParserError, KeyError
|
|
44
|
+
nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def stored(result)
|
|
48
|
+
{ "met" => result.met, "checked_at" => result.checked_at.utc.iso8601, "fingerprint" => result.fingerprint }
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
5
|
+
module DevOnboarder
|
|
6
|
+
module RecordLocation
|
|
7
|
+
FILE = "dev_onboarder.json"
|
|
8
|
+
|
|
9
|
+
def self.path(root)
|
|
10
|
+
git_directory, status = Open3.capture2("git", "-C", root.to_s, "rev-parse", "--absolute-git-dir",
|
|
11
|
+
err: File::NULL)
|
|
12
|
+
return File.join(root, ".#{FILE}") unless status.success?
|
|
13
|
+
|
|
14
|
+
File.join(git_directory.chomp, FILE)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module DevOnboarder
|
|
7
|
+
Requirement = Data.define(:key, :group, :purpose, :check, :fix, :instruction, :feature, :variable,
|
|
8
|
+
:optional, :timeout) do
|
|
9
|
+
def initialize(key:, group:, purpose:, check:, fix: nil, instruction: nil, feature: nil, variable: nil,
|
|
10
|
+
optional: false, timeout: nil)
|
|
11
|
+
super
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def fixable?
|
|
15
|
+
!fix.nil?
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def fingerprint
|
|
19
|
+
Digest::SHA256.hexdigest([check, fix, instruction].to_json)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../dev_onboarder"
|
|
4
|
+
require_relative "machine_tools"
|
|
5
|
+
require_relative "requirement"
|
|
6
|
+
require_relative "secrets"
|
|
7
|
+
|
|
8
|
+
module DevOnboarder
|
|
9
|
+
Feature = Data.define(:name, :description)
|
|
10
|
+
|
|
11
|
+
class Requirements
|
|
12
|
+
include Enumerable
|
|
13
|
+
include MachineTools
|
|
14
|
+
include Secrets
|
|
15
|
+
|
|
16
|
+
attr_reader :features
|
|
17
|
+
|
|
18
|
+
def self.load(path)
|
|
19
|
+
raise Error, "This repo has no Setupfile, so there is nothing to set up." unless File.exist?(path)
|
|
20
|
+
|
|
21
|
+
evaluate(path)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.evaluate(path)
|
|
25
|
+
new(path).tap { |requirements| requirements.instance_eval(File.read(path), path) }
|
|
26
|
+
rescue ScriptError, StandardError => e
|
|
27
|
+
raise Error, "Setupfile line #{line_of(e, path)}: #{reason_of(e, path)}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.line_of(error, path)
|
|
31
|
+
error.backtrace_locations&.find { |location| location.path == path }&.lineno ||
|
|
32
|
+
error.message[/#{Regexp.escape(path)}:(\d+)/, 1]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.reason_of(error, path)
|
|
36
|
+
error.message.lines.first.chomp.sub(/\A#{Regexp.escape(path)}:\d+: /, "")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def initialize(path)
|
|
40
|
+
@path = path
|
|
41
|
+
@dir = File.dirname(path)
|
|
42
|
+
@lines = {}
|
|
43
|
+
@declared = []
|
|
44
|
+
@features = []
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def requirement(key, **attributes)
|
|
48
|
+
line = caller_locations.find { |location| location.path == @path }&.lineno
|
|
49
|
+
raise Error, "#{key} is already declared on line #{@lines[key]}" if @lines.key?(key)
|
|
50
|
+
|
|
51
|
+
@lines[key] = line
|
|
52
|
+
@declared << Requirement.new(key: key, feature: @current_feature, **attributes)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def feature(name, description)
|
|
56
|
+
@features << Feature.new(name: name, description: description)
|
|
57
|
+
@current_feature = name
|
|
58
|
+
yield
|
|
59
|
+
ensure
|
|
60
|
+
@current_feature = nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def each(&)
|
|
64
|
+
@declared.each(&)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "io/console"
|
|
4
|
+
require_relative "env_file"
|
|
5
|
+
|
|
6
|
+
module DevOnboarder
|
|
7
|
+
class SecretPrompt
|
|
8
|
+
def initialize(env_file:, input:, out:, shell:)
|
|
9
|
+
@env_file = env_file
|
|
10
|
+
@input = input
|
|
11
|
+
@out = out
|
|
12
|
+
@shell = shell
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def collect(requirement)
|
|
16
|
+
return unless @input.tty?
|
|
17
|
+
return @out.puts(not_ignored(requirement)) unless ignored_by_git?
|
|
18
|
+
|
|
19
|
+
@out.puts "#{requirement.variable} — #{requirement.instruction}"
|
|
20
|
+
@out.print "Value (what you type is not shown): "
|
|
21
|
+
value = typed_value
|
|
22
|
+
@out.puts
|
|
23
|
+
@env_file.set(requirement.variable, value) unless value.empty?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def not_ignored(requirement)
|
|
29
|
+
"#{requirement.variable} — add .env to .gitignore, then run setup again to enter its value."
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def ignored_by_git?
|
|
33
|
+
@shell.succeeds?("git check-ignore --quiet .env")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def typed_value
|
|
37
|
+
@input.noecho(&:gets).to_s.chomp
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DevOnboarder
|
|
4
|
+
module Secrets
|
|
5
|
+
def env(name, from:, optional: false)
|
|
6
|
+
requirement name.downcase.to_sym, group: :secrets, purpose: "#{name} is set",
|
|
7
|
+
check: %(test -n "${#{name}:-}" || grep -q "^#{name}=." .env 2>/dev/null),
|
|
8
|
+
instruction: "Get it from #{from}.", variable: name, optional: optional
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def key_file(path, from:)
|
|
12
|
+
requirement path.gsub(/\W/, "_").to_sym, group: :secrets, purpose: "#{path} exists", check: %(test -f "#{path}"),
|
|
13
|
+
instruction: "Ask #{from} for #{path}."
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "requirement"
|
|
4
|
+
require_relative "record"
|
|
5
|
+
|
|
6
|
+
module DevOnboarder
|
|
7
|
+
Outcome = Data.define(:requirement, :met, :last_checked_at, :fix_output)
|
|
8
|
+
|
|
9
|
+
class Setup
|
|
10
|
+
def initialize(requirements:, record:, shell:, clock:, feature: nil, secrets: nil)
|
|
11
|
+
@requirements = feature ? requirements.select { |requirement| requirement.feature == feature } : requirements
|
|
12
|
+
@feature = feature
|
|
13
|
+
@record = record
|
|
14
|
+
@shell = shell
|
|
15
|
+
@clock = clock
|
|
16
|
+
@secrets = secrets
|
|
17
|
+
@fix_outputs = {}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def call
|
|
21
|
+
outcomes = @requirements.map { |requirement| outcome_for(requirement) }
|
|
22
|
+
@record.save(results_of(outcomes))
|
|
23
|
+
outcomes
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def results_of(outcomes)
|
|
29
|
+
checked_at = @clock.call
|
|
30
|
+
outcomes.to_h { |outcome| [outcome.requirement.key, result_of(outcome, checked_at)] }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def result_of(outcome, checked_at)
|
|
34
|
+
Result.new(met: outcome.met, checked_at: checked_at, fingerprint: outcome.requirement.fingerprint)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def outcome_for(requirement)
|
|
38
|
+
Outcome.new(requirement: requirement, met: met?(requirement), fix_output: @fix_outputs[requirement.key],
|
|
39
|
+
last_checked_at: @record.result_for(requirement.key)&.checked_at)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def met?(requirement)
|
|
43
|
+
return true if check_passes?(requirement)
|
|
44
|
+
return false unless requirement.feature == @feature
|
|
45
|
+
return supplied?(requirement) unless requirement.fixable?
|
|
46
|
+
|
|
47
|
+
run_fix(requirement)
|
|
48
|
+
check_passes?(requirement)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def run_fix(requirement)
|
|
52
|
+
run = @shell.run(requirement.fix, timeout: requirement.timeout)
|
|
53
|
+
@fix_outputs[requirement.key] = run.output unless run.success
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def supplied?(requirement)
|
|
57
|
+
return false unless @secrets && requirement.variable
|
|
58
|
+
|
|
59
|
+
@secrets.collect(requirement)
|
|
60
|
+
check_passes?(requirement)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def check_passes?(requirement)
|
|
64
|
+
@shell.succeeds?(requirement.check)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
5
|
+
module DevOnboarder
|
|
6
|
+
class Shell
|
|
7
|
+
Run = Data.define(:success, :output)
|
|
8
|
+
|
|
9
|
+
def succeeds?(command)
|
|
10
|
+
system(command, out: File::NULL, err: File::NULL) == true
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def run(command, timeout: nil)
|
|
14
|
+
Open3.popen2e(command, pgroup: true) do |input, output, process|
|
|
15
|
+
input.close
|
|
16
|
+
printed = Thread.new { output.read }
|
|
17
|
+
next Run.new(success: process.value.success?, output: printed.value) if process.join(timeout)
|
|
18
|
+
|
|
19
|
+
Process.kill("TERM", -process.pid)
|
|
20
|
+
Run.new(success: false, output: "#{printed.value}Stopped after #{timeout} seconds.\n")
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "requirement"
|
|
4
|
+
require_relative "record"
|
|
5
|
+
|
|
6
|
+
module DevOnboarder
|
|
7
|
+
Finding = Data.define(:requirement, :state)
|
|
8
|
+
|
|
9
|
+
class Status
|
|
10
|
+
def initialize(requirements:, record:)
|
|
11
|
+
@requirements = requirements
|
|
12
|
+
@record = record
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call
|
|
16
|
+
@requirements.filter_map do |requirement|
|
|
17
|
+
state = state_of(requirement)
|
|
18
|
+
Finding.new(requirement: requirement, state: state) if state
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def state_of(requirement)
|
|
25
|
+
result = @record.result_for(requirement.key)
|
|
26
|
+
return :new unless result
|
|
27
|
+
|
|
28
|
+
return :changed unless result.fingerprint == requirement.fingerprint
|
|
29
|
+
|
|
30
|
+
:not_met unless result.met
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dev_onboarder-develop
|
|
3
|
+
description: Use PROACTIVELY for declaring what a repo needs to run — writing or editing its Setupfile with requirements, machine tools, the Ruby version, Brewfile packages, environment variables, key files and per-feature requirements — and for running, reading or explaining the setup, status and features commands and the setup page. MUST BE USED instead of hand-writing bin/setup scripts, README setup checklists, or ad hoc shell checks for a new developer's machine.
|
|
4
|
+
tools: Read, Write, Edit, Grep
|
|
5
|
+
scope: repo setup — the Setupfile that declares what a repo needs, and the command a new developer runs to check and fix it
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
This local writes and edits the repo's `Setupfile` and tells the developer which command to run. It follows the steps below exactly and invents none. Where a step names a decision, it asks the developer and does not pick.
|
|
9
|
+
|
|
10
|
+
## What dev_onboarder is
|
|
11
|
+
|
|
12
|
+
A gem that reads a `Setupfile` at the repo's root, checks each requirement it declares, runs the fix for each one that is not met, and records what it found so the next run can say what changed. Fire this local when someone wants a new developer's clone to check or fix itself: a tool to install, a database to create, a key to obtain, an environment variable to set, or something only one feature needs. Adding the gem to the repo is the install local's job, not this one.
|
|
13
|
+
|
|
14
|
+
## Interface
|
|
15
|
+
|
|
16
|
+
- `Setupfile` — the Ruby file at the repo's root that declares every requirement, one call per line.
|
|
17
|
+
- `requirement` — declares one requirement by key, with a shell command that checks it and optionally one that fixes it.
|
|
18
|
+
- `program` — requires a program on the path, optionally at a minimum version, with the command that installs it.
|
|
19
|
+
- `ruby_version` — requires the running Ruby to be the one named in the repo's `.ruby-version`.
|
|
20
|
+
- `brewfile` — requires every package in the repo's `Brewfile` and installs the missing ones with Homebrew.
|
|
21
|
+
- `env` — requires an environment variable, set in the shell or in the repo's `.env`, and asks for it when missing.
|
|
22
|
+
- `key_file` — requires a file to exist and says who to ask for it.
|
|
23
|
+
- `feature` — a block whose requirements are needed only to work on one feature.
|
|
24
|
+
- `bundle exec dev_onboarder` — checks every requirement, fixes the ones outside any feature, and records the result.
|
|
25
|
+
- `bundle exec dev_onboarder status` — lists what is new, changed or not met since the last run, without checking or fixing anything.
|
|
26
|
+
- `bundle exec dev_onboarder features` — lists each feature as ready or not ready, from the record of the last run.
|
|
27
|
+
- `bundle exec dev_onboarder setup` — checks and fixes one feature's requirements when given its name, and behaves like the bare command when not.
|
|
28
|
+
- `/dev_onboarder` — a read-only page showing the last recorded run, present only in a Rails app that has keystone_ui, and only when it runs locally.
|
|
29
|
+
|
|
30
|
+
## How to use it
|
|
31
|
+
|
|
32
|
+
1. Read the repo's existing `Setupfile` if there is one, and keep every key already in it. Keys must be unique across the whole file, including inside `feature` blocks, and a duplicate is refused with the line of each. Without a `Setupfile`, every command prints `This repo has no Setupfile, so there is nothing to set up.` and exits non-zero.
|
|
33
|
+
|
|
34
|
+
2. Ask the developer what the repo needs that is not already declared. For each item, pick the one-line form below that fits, and fall back to `requirement` only when none does.
|
|
35
|
+
|
|
36
|
+
3. Declare machine tools:
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
program "psql", version: "17", install: "brew install postgresql@17"
|
|
40
|
+
ruby_version
|
|
41
|
+
brewfile
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
- `program(name, version: nil, install: nil)` — `version` is the oldest version accepted, compared against the first dotted number in the output of `<name> --version`. Leave it out to require only that the program is on the path. `install` is the command run when the program is missing or too old; without it the program is checked and never installed. Its key is the name with `-` written as `_`.
|
|
45
|
+
- `ruby_version` takes no arguments. The repo must have a `.ruby-version` file, or the `Setupfile` fails to load. It never fixes anything and shows both Ruby versions when they differ. Its key is `ruby_version`.
|
|
46
|
+
- `brewfile` takes no arguments. The repo must have a `Brewfile` at its root, or the `Setupfile` fails to load. Without Homebrew it installs nothing and lists the packages to install by hand. Its key is `brewfile`.
|
|
47
|
+
|
|
48
|
+
4. Declare secrets and keys:
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
env "PRICE_KEY", from: "the vendor dashboard, under API keys"
|
|
52
|
+
env "MAP_KEY", from: "the team lead", optional: true
|
|
53
|
+
key_file "config/master.key", from: "the team lead"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
- `env(name, from:, optional: false)` — `from` is required and is shown as "Get it from <from>." Setup asks for a missing value only when a keyboard is attached, does not echo it, and writes it to `.env` only. An empty value writes nothing. Its key is the name in lower case.
|
|
57
|
+
- Setup writes to `.env` only when git ignores `.env`. Check the repo's `.gitignore`. If `.env` is not ignored, ask the developer whether to add it, since setup will otherwise only print `<NAME> — add .env to .gitignore, then run setup again to enter its value.`
|
|
58
|
+
- `.env` must hold only `NAME=value` lines, with no comments, blank lines or quotes. Setup rewrites the whole file in that form when it stores a value.
|
|
59
|
+
- In a Rails app that has keystone_ui, `.env` is loaded into the environment when the app boots in development, before the app's initializers, and a variable the shell already sets is kept.
|
|
60
|
+
- `optional: true` lists the variable when missing and never fails a run or status. Ask the developer which variables are optional; do not decide it.
|
|
61
|
+
- `key_file(path, from:)` — `from` is required. The path is relative to the repo's root. It is checked and never fixed. Its key is the path with every non-word character written as `_`.
|
|
62
|
+
- Ask the developer where each value is obtained. The `from` text is what a new developer reads, so it must name a real person or place.
|
|
63
|
+
|
|
64
|
+
5. Declare anything else with `requirement`:
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
requirement :databases, group: :repo_setup, purpose: "Development and test databases exist",
|
|
68
|
+
check: "bin/rails db:version", fix: "bin/rails db:prepare", timeout: 120
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
- Required: the key as a symbol, `group:` as a symbol, `purpose:` as one line a person reads, and `check:` as a shell command that exits zero when the requirement is met.
|
|
72
|
+
- Optional: `fix:` is a shell command run when the check fails, after which the check runs again. `instruction:` is shown while the requirement is still not met. `timeout:` is the fix's limit in seconds, after which it is stopped and reported as failed. `optional: true` keeps it from failing a run or status.
|
|
73
|
+
- Never pass `feature:` to `requirement`. A requirement belongs to a feature only by being inside that feature's block.
|
|
74
|
+
- Checks and fixes run from the directory the command is run in, which is the repo's root. Write paths relative to it.
|
|
75
|
+
- A requirement with neither a fix nor an instruction leaves a new developer with no next step. Ask the developer for one of the two.
|
|
76
|
+
- Ask the developer which group each requirement belongs to. The page groups requirements by it. The groups the one-line forms use are `:machine_tools` and `:secrets`.
|
|
77
|
+
|
|
78
|
+
6. Put a requirement needed only to work on one feature inside a `feature` block:
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
feature :payments, "Take a test payment" do
|
|
82
|
+
env "PAYMENT_KEY", from: "the payment dashboard, test mode"
|
|
83
|
+
end
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
- `feature(name, description)` takes a symbol and one line a person reads. Every form from steps 3 to 5 works inside it.
|
|
87
|
+
- Ask the developer which requirements belong to a feature rather than to the whole repo.
|
|
88
|
+
|
|
89
|
+
7. Tell the developer to run the command from the repo's root:
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
bundle exec dev_onboarder
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
- It lists each requirement as met or not met, with the time of the previous run's check when there was one, shows the output of a fix that failed, and shows the instruction for each one still not met.
|
|
96
|
+
- It checks a feature's requirements but runs none of their fixes, asks for none of their values, and does not fail because of them.
|
|
97
|
+
- It exits non-zero when any required requirement outside a feature is still not met.
|
|
98
|
+
- An error in the `Setupfile` is printed as `Setupfile line N: <reason>` and the command exits non-zero.
|
|
99
|
+
|
|
100
|
+
8. Use the other commands for the case each covers:
|
|
101
|
+
|
|
102
|
+
- `bundle exec dev_onboarder setup payments` checks, fixes and asks for only that feature's requirements, and exits non-zero when any required one of them is not met. An unknown name prints `No feature named <name>.` and exits non-zero.
|
|
103
|
+
- `bundle exec dev_onboarder status` lists each requirement that is new, changed or was left not met, with a feature's requirements under its name. A requirement counts as changed when its check, fix or instruction changes. With nothing to list it prints `Nothing has changed since your last setup.` It exits non-zero only when a required requirement outside a feature is listed.
|
|
104
|
+
- `bundle exec dev_onboarder features` lists each feature as ready or not ready. A feature is not ready when any of its requirements is new, changed or not met. It exits zero whenever the `Setupfile` loads.
|
|
105
|
+
|
|
106
|
+
9. In a Rails app that has keystone_ui, point the developer to `/dev_onboarder` on the locally running app. It shows every requirement by group with its state and last check, each feature and whether it is ready, and the command to run. It reads the last recorded run and runs no check and no fix, so a change to the `Setupfile` appears there only after a command has run. An error in the `Setupfile` is shown on the page as a warning. When the server starts locally, its output carries one line when any requirement outside a feature is new, changed or not met.
|
|
107
|
+
|
|
108
|
+
## Conventions
|
|
109
|
+
|
|
110
|
+
- Every requirement is declared in the `Setupfile`. Never add setup steps to `bin/setup`, the README, or a separate script.
|
|
111
|
+
- A `check` must exit zero only when the requirement is met, and must not change anything. Changes go in `fix`.
|
|
112
|
+
- A `fix` must be safe to run again on a machine where it already ran.
|
|
113
|
+
- Never put a secret value in the `Setupfile`. Declare it with `env` or `key_file`.
|
|
114
|
+
- Never commit `.env` or the setup record. The record lives inside the clone's git directory, or at `.dev_onboarder.json` at the root outside git, and is never edited by hand.
|
|
115
|
+
- Each git worktree keeps its own record, so a requirement met in one worktree shows as new in another until the command runs there.
|
|
116
|
+
- Adding the gem to the `Gemfile` and choosing the controller the setup page is drawn under are out of scope; they belong to the install local.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dev_onboarder-info
|
|
3
|
+
description: Use to learn what dev_onboarder offers — declaring what a repo needs to run, checking and fixing it from one command, per-feature setup, the record of the last run, and the local setup page and startup notice in a Rails app.
|
|
4
|
+
tools: Read
|
|
5
|
+
scope: repo setup — the Setupfile that declares what a repo needs, and the command a new developer runs to check and fix it
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
This local explains what dev_onboarder is and the words its other two locals use. It makes no changes and gives no steps.
|
|
9
|
+
|
|
10
|
+
## What dev_onboarder is
|
|
11
|
+
|
|
12
|
+
dev_onboarder takes a developer from a fresh clone to a working repo with one command. The repo lists what it needs in one file at its root: databases that exist, programs on the path at a minimum version, the Ruby the repo names, Homebrew packages, environment variables, key files. The command checks each need, runs the repo's own fix for any that are not met, asks for the value of a missing variable, checks again, and says what is still missing and who to ask.
|
|
13
|
+
|
|
14
|
+
Reach for it when a repo's setup lives in a README, in someone's memory, or in a script that stops at the first failure. It works in any repo, Rails or not, through the command alone. A Rails app that also has keystone_ui gets three more things: it reads the `.env` file in development before the app's initializers run, it prints one line at local server start when needs outside any feature are new, changed or not met, and it gets a read-only page showing the last run, which exists only when the app runs locally. A Rails app without keystone_ui gets none of the three. Adding the gem changes no other file in the repo.
|
|
15
|
+
|
|
16
|
+
## Interface
|
|
17
|
+
|
|
18
|
+
This local declares no commands. The other two locals own the whole surface:
|
|
19
|
+
|
|
20
|
+
- **install** owns adding the gem to a repo and the one setting a Rails app can change, the controller the setup page is drawn under.
|
|
21
|
+
- **develop** owns everything written in the setup file, each kind of declaration, and every command a developer runs.
|
|
22
|
+
|
|
23
|
+
## How to use it
|
|
24
|
+
|
|
25
|
+
- The gem is not in the repo yet, or the setup page needs a different controller: use the install local.
|
|
26
|
+
- The repo has the gem and you need to declare a need, group needs under a feature, or run, read or explain the command: use the develop local.
|
|
27
|
+
- Neither: the answer is in this page.
|
|
28
|
+
|
|
29
|
+
## Conventions
|
|
30
|
+
|
|
31
|
+
- **Setup file** — the Ruby file at the repo's root, named in the scope line above, where every need is declared. One declaration per need.
|
|
32
|
+
- **Requirement** — one need, identified by a key that must be unique across the file. It carries a group, a one-line purpose, and a check. It may carry a fix, an instruction, and a time limit on the fix in seconds.
|
|
33
|
+
- **Check** — a shell command that exits zero when the requirement is met.
|
|
34
|
+
- **Fix** — a shell command run when the check fails, after which the check runs again.
|
|
35
|
+
- **Instruction** — text shown when the requirement is still not met, usually who to ask.
|
|
36
|
+
- **Group** — a label that sorts requirements for display. Built-in declarations use `machine_tools` for programs, Ruby and Homebrew packages, and `secrets` for environment variables and key files. Others are chosen by the repo.
|
|
37
|
+
- **Optional** — a variable that is listed when missing and never fails a run.
|
|
38
|
+
- **Met / not met** — the result of one requirement's check on the last run.
|
|
39
|
+
- **Feature** — a named group of requirements needed only to test one part of the app. A plain run checks them, runs none of their fixes, and does not fail on them. A feature is **ready** when none of its requirements is new, changed or not met.
|
|
40
|
+
- **Setup record** — what the last run found, kept inside the clone's git directory so it is never committed, or at the repo's root outside git. Each git worktree keeps its own. A run writes it once, at the end.
|
|
41
|
+
- **New / changed / not met** — the three states a status report shows by comparing the setup file with the setup record. A requirement is **changed** when its check, fix or instruction differs from what the last run recorded.
|
|
42
|
+
- **Setup page** — the read-only page in a Rails app with keystone_ui. It shows the setup record and runs no check and no fix.
|
|
43
|
+
- **Startup notice** — the one line a local Rails server with keystone_ui prints at start, counting the needs outside any feature that need setup.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dev_onboarder-install
|
|
3
|
+
description: Use to hook dev_onboarder into a project — adding the gem to the Gemfile, and in a Rails app with keystone_ui, setting the controller the setup page is drawn under.
|
|
4
|
+
tools: Bash, Read, Edit
|
|
5
|
+
scope: repo setup — the Setupfile that declares what a repo needs, and the command a new developer runs to check and fix it
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
This local follows the steps below exactly and invents none. Where a step names a decision, it asks the developer and does not pick.
|
|
9
|
+
|
|
10
|
+
## What dev_onboarder is
|
|
11
|
+
|
|
12
|
+
A gem that checks and fixes what a repo needs to run, hooked into any repo that has a `Gemfile`, with a read-only setup page, `.env` loading and a server startup line added in a Rails app that has keystone_ui.
|
|
13
|
+
|
|
14
|
+
## Interface
|
|
15
|
+
|
|
16
|
+
- `gem "dev_onboarder", github: "DYB-Development/dev_onboarder", tag: "v0.9.0"` — the `Gemfile` line that adds the gem to the repo.
|
|
17
|
+
- `DevOnboarder.base_controller` — the name of the controller the setup page inherits from, given as a string, in a Rails app with keystone_ui only.
|
|
18
|
+
|
|
19
|
+
## How to use it
|
|
20
|
+
|
|
21
|
+
1. Read the repo's `Gemfile`. If it already has a `dev_onboarder` line, stop and tell the developer which version it names.
|
|
22
|
+
2. Ask the developer which Gemfile group the gem goes in: the default group, or `group: :development`. In the default group, the setup page also exists when the app runs in test. In `group: :development`, the gem is not loaded in test or production.
|
|
23
|
+
3. Add this line to the `Gemfile`, in the group the developer chose:
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
gem "dev_onboarder", github: "DYB-Development/dev_onboarder", tag: "v0.9.0"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
4. Run `bundle install`. It updates `Gemfile.lock`. No other file in the repo is created or edited: no route, no initializer, no boot code, no `.gitignore` line.
|
|
30
|
+
5. If the repo is not a Rails app, stop here.
|
|
31
|
+
6. If the Rails app does not have `keystone_ui` in its `Gemfile.lock`, ask the developer whether they want the setup page, the `.env` loading and the startup line. All three exist only when keystone_ui is in the bundle, and the page is tested against keystone_ui 0.30. If they want them, hand off to the keystone_ui install local to add keystone_ui, then continue. If not, stop here: the command works without them.
|
|
32
|
+
7. With keystone_ui in the bundle, the setup page inherits from `ApplicationController` when the app defines one, and from `ActionController::Base` otherwise. It is drawn inside that controller's layout, and that controller's before-actions, such as sign-in checks, run on the page. Ask the developer whether the page should inherit from another controller. If not, change nothing. If so, create `config/initializers/dev_onboarder.rb` with the controller's name as a string, guarded so any environment that does not load the page still boots:
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
DevOnboarder.base_controller = "AdminController" if defined?(DevOnboarder::Engine)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Conventions
|
|
39
|
+
|
|
40
|
+
- Confirm the install with `bundle info dev_onboarder`, which prints version 0.9.0.
|
|
41
|
+
- In a Rails app where step 7 set a controller, confirm it with `bin/rails runner -e development 'puts DevOnboarder.base_controller'`, which prints that controller's name.
|
|
42
|
+
- `DevOnboarder.base_controller` exists only when the app is a Rails app and keystone_ui is in the bundle. Never set it without the `defined?(DevOnboarder::Engine)` guard.
|
|
43
|
+
- The setup page is mounted at `/dev_onboarder` by the gem itself when the app runs in development or test, and never in production. Never add a route for it.
|
|
44
|
+
- In a Rails app with keystone_ui, the gem reads the repo's `.env` into the environment in development, before the app's own initializers, and leaves any variable the shell already sets as it is. Tell the developer this when the app already loads `.env` another way.
|
|
45
|
+
- In a Rails app with keystone_ui and a `Setupfile`, a server started in development or test prints one line when a requirement outside any feature is new, changed or not met. Nothing is added to the app for it.
|
|
46
|
+
- To move to a newer release, change the `tag:` in the `Gemfile` line and run `bundle update dev_onboarder`.
|
|
47
|
+
- Writing the `Setupfile`, and running or reading the gem's command, are out of scope. Hand those to the develop local.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
scope: repo setup — the Setupfile that declares what a repo needs, and the command a new developer runs to check and fix it
|
|
2
|
+
|
|
3
|
+
install:
|
|
4
|
+
- 'gem "dev_onboarder", github: "DYB-Development/dev_onboarder", tag: "v0.9.0"'
|
|
5
|
+
- DevOnboarder.base_controller
|
|
6
|
+
|
|
7
|
+
develop:
|
|
8
|
+
- Setupfile
|
|
9
|
+
- requirement
|
|
10
|
+
- program
|
|
11
|
+
- ruby_version
|
|
12
|
+
- brewfile
|
|
13
|
+
- env
|
|
14
|
+
- key_file
|
|
15
|
+
- feature
|
|
16
|
+
- bundle exec dev_onboarder
|
|
17
|
+
- bundle exec dev_onboarder status
|
|
18
|
+
- bundle exec dev_onboarder features
|
|
19
|
+
- bundle exec dev_onboarder setup
|
|
20
|
+
- /dev_onboarder
|
|
21
|
+
|
|
22
|
+
sources:
|
|
23
|
+
- README.md
|
|
24
|
+
- lib/dev_onboarder.rb
|
|
25
|
+
- lib/dev_onboarder/version.rb
|
|
26
|
+
- lib/dev_onboarder/cli.rb
|
|
27
|
+
- lib/dev_onboarder/requirements.rb
|
|
28
|
+
- lib/dev_onboarder/requirement.rb
|
|
29
|
+
- lib/dev_onboarder/machine_tools.rb
|
|
30
|
+
- lib/dev_onboarder/secrets.rb
|
|
31
|
+
- lib/dev_onboarder/setup.rb
|
|
32
|
+
- lib/dev_onboarder/status.rb
|
|
33
|
+
- lib/dev_onboarder/record_location.rb
|
|
34
|
+
- lib/dev_onboarder/engine.rb
|
|
35
|
+
- config/routes.rb
|
|
36
|
+
- app/controllers/dev_onboarder/setup_controller.rb
|