runsheets 0.0.1 → 0.0.2
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/Archspec.rb +137 -0
- data/CHANGELOG.md +35 -1
- data/README.md +7 -3
- data/archspec_todo.yml +32 -0
- data/lib/runsheets/cli.rb +98 -164
- data/lib/runsheets/config/defaults.yml +0 -2
- data/lib/runsheets/config.rb +55 -16
- data/lib/runsheets/prompt.rb +62 -0
- data/lib/runsheets/runbook.rb +12 -0
- data/lib/runsheets/single_file.rb +34 -1
- data/lib/runsheets/slug.rb +19 -0
- data/lib/runsheets/step.rb +1 -1
- data/lib/runsheets/step_runner.rb +261 -0
- data/lib/runsheets/template.rb +94 -0
- data/lib/runsheets/version.rb +1 -1
- data/lib/runsheets/wizard.rb +260 -0
- data/lib/runsheets.rb +9 -4
- data/templates/asgard/.loki.erb +9 -0
- data/templates/asgard/runbook.md.erb +23 -0
- data/templates/asgard/steps/010-preflight.md.erb +13 -0
- data/templates/asgard/steps/020-do-the-work.md.erb +16 -0
- data/templates/asgard/steps/030-tell-people.md.erb +7 -0
- data/templates/asgard/tasks.loki +28 -0
- data/templates/asgard/template.yml +2 -0
- data/templates/asgard/verify.md.erb +12 -0
- data/templates/checks/runbook.md.erb +15 -0
- data/templates/checks/steps/010-tools.md.erb +17 -0
- data/templates/checks/steps/020-disk-space.md.erb +14 -0
- data/templates/checks/steps/030-writable.md.erb +13 -0
- data/templates/checks/template.yml +2 -0
- data/templates/inputs/runbook.md.erb +40 -0
- data/templates/inputs/steps/010-preflight.md.erb +15 -0
- data/templates/inputs/steps/020-use-the-inputs.md.erb +13 -0
- data/templates/inputs/steps/030-another-language.md.erb +13 -0
- data/templates/inputs/template.yml +2 -0
- data/templates/inputs/verify.md.erb +12 -0
- data/templates/rollback/rollback.md.erb +10 -0
- data/templates/rollback/runbook.md.erb +29 -0
- data/templates/rollback/steps/010-preflight.md.erb +16 -0
- data/templates/rollback/steps/020-keep-a-copy.md.erb +18 -0
- data/templates/rollback/steps/030-replace.md.erb +14 -0
- data/templates/rollback/steps/040-tell-people.md.erb +7 -0
- data/templates/rollback/template.yml +2 -0
- data/templates/rollback/verify.md.erb +15 -0
- data/templates/tasks/runbook.md.erb +29 -0
- data/templates/tasks/steps/010-preflight.md.erb +16 -0
- data/templates/tasks/steps/020-do-the-work.md.erb +18 -0
- data/templates/tasks/steps/030-tell-people.md.erb +7 -0
- data/templates/tasks/template.yml +2 -0
- data/templates/tasks/verify.md.erb +15 -0
- data/templates/timeouts/runbook.md.erb +30 -0
- data/templates/timeouts/steps/010-preflight.md.erb +16 -0
- data/templates/timeouts/steps/020-watch-progress.md.erb +13 -0
- data/templates/timeouts/steps/030-the-long-step.md.erb +20 -0
- data/templates/timeouts/steps/040-look-it-over.md.erb +14 -0
- data/templates/timeouts/template.yml +2 -0
- data/templates/timeouts/verify.md.erb +13 -0
- metadata +48 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 92cd3b7a77a52879a46fe002837ab2c7f0df600865eed44ed31f6b2b75491ff3
|
|
4
|
+
data.tar.gz: e70a7b616393d87c1d6fa83e4952e73da0f33da1dd3aab454aa774f03ac197aa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2737dd58316509fb19cf8897b41d3983d538011a64725bd7320fd0e61429e42664063894d0e6f5e70acfd3078df4c222b07fb6552b249b7c6aef7b7978fa224c
|
|
7
|
+
data.tar.gz: aee18d7c0923c297ec38ffa5ffe6b28b59a75a9b19831c3a48c42168ea0b099edd13955fdaadbdd01c1ded1a599e936c1dbddbe19bdf603b04bdb6245a18c8d4
|
data/Archspec.rb
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Executable architecture for runsheets, checked by the archspec gem:
|
|
4
|
+
#
|
|
5
|
+
# archspec check
|
|
6
|
+
# archspec explain lib/runsheets/session.rb
|
|
7
|
+
#
|
|
8
|
+
# docs/reference/architecture.md describes the design; this file enforces it.
|
|
9
|
+
# runsheets is a gem: a document model, an execution layer, a record writer,
|
|
10
|
+
# a session over them, and two front ends (a Sinatra app and the terminal)
|
|
11
|
+
# wired together by the CLI. Dependencies point inward only:
|
|
12
|
+
#
|
|
13
|
+
# cli ─┬─> routes ─> pages ─┐
|
|
14
|
+
# └─> console ─────────┴─> session ─┬─> execution ─┐
|
|
15
|
+
# ├─> recording ─┤
|
|
16
|
+
# └─> document ──┴─> foundation
|
|
17
|
+
#
|
|
18
|
+
# Pages and routes may also read the document and recording layers directly
|
|
19
|
+
# (rendering, history); nothing outside session reaches execution.
|
|
20
|
+
|
|
21
|
+
root "."
|
|
22
|
+
source "lib/**/*.rb"
|
|
23
|
+
|
|
24
|
+
# Known violations, burned down rather than allowed: refresh with
|
|
25
|
+
# `archspec check --update-todo` only after fixing one.
|
|
26
|
+
todo "archspec_todo.yml"
|
|
27
|
+
|
|
28
|
+
# Shared building blocks with no runsheets dependencies beyond each other:
|
|
29
|
+
# the Runsheets module itself, settings, parsing helpers, the redactor, and the
|
|
30
|
+
# session log (a plain Logger wrapper that Config borrows LEVELS from).
|
|
31
|
+
component :foundation, in: %w[
|
|
32
|
+
lib/runsheets.rb
|
|
33
|
+
lib/runsheets/version.rb
|
|
34
|
+
lib/runsheets/config.rb
|
|
35
|
+
lib/runsheets/front_matter.rb
|
|
36
|
+
lib/runsheets/fences.rb
|
|
37
|
+
lib/runsheets/slug.rb
|
|
38
|
+
lib/runsheets/block.rb
|
|
39
|
+
lib/runsheets/diff.rb
|
|
40
|
+
lib/runsheets/expectation.rb
|
|
41
|
+
lib/runsheets/redactor.rb
|
|
42
|
+
lib/runsheets/session_log.rb
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
# The document model: markdown files rendered into steps, runbooks, libraries.
|
|
46
|
+
component :document, in: %w[
|
|
47
|
+
lib/runsheets/renderer.rb
|
|
48
|
+
lib/runsheets/step.rb
|
|
49
|
+
lib/runsheets/single_file.rb
|
|
50
|
+
lib/runsheets/runbook.rb
|
|
51
|
+
lib/runsheets/library.rb
|
|
52
|
+
lib/runsheets/search.rb
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
# Spawning, pumping, reaping and timing out a block's process.
|
|
56
|
+
component :execution, in: %w[
|
|
57
|
+
lib/runsheets/execution.rb
|
|
58
|
+
lib/runsheets/executor.rb
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
# The runsheet: run.json, run.md and blocks/ on disk.
|
|
62
|
+
component :recording, in: %w[lib/runsheets/run_record.rb]
|
|
63
|
+
|
|
64
|
+
# The engineering session and each runbook's run inside it.
|
|
65
|
+
component :session, in: %w[
|
|
66
|
+
lib/runsheets/run.rb
|
|
67
|
+
lib/runsheets/session.rb
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
# Pure functions from a session to HTML, plus the inline CSS and JavaScript.
|
|
71
|
+
component :pages, in: %w[
|
|
72
|
+
lib/runsheets/pages.rb
|
|
73
|
+
lib/runsheets/pages/**/*.rb
|
|
74
|
+
lib/runsheets/assets.rb
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
# The Sinatra app: routes and the host/token security checks.
|
|
78
|
+
component :routes, in: %w[lib/runsheets/web.rb]
|
|
79
|
+
|
|
80
|
+
# Terminal front end: prompts, templates, --new and --run.
|
|
81
|
+
component :console, in: %w[
|
|
82
|
+
lib/runsheets/prompt.rb
|
|
83
|
+
lib/runsheets/template.rb
|
|
84
|
+
lib/runsheets/wizard.rb
|
|
85
|
+
lib/runsheets/step_runner.rb
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
# Option parsing and the bin/runsheets entry point; the composition root.
|
|
89
|
+
component :cli, in: %w[lib/runsheets/cli.rb]
|
|
90
|
+
|
|
91
|
+
# ── Layering ────────────────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
foundation.can_only_use(because: "the foundation is shared by every layer and depends on none")
|
|
94
|
+
document.can_only_use :foundation,
|
|
95
|
+
because: "the document model is tested in isolation and knows nothing of runs or front ends"
|
|
96
|
+
execution.can_only_use :foundation,
|
|
97
|
+
because: "the executor runs code it is handed; it does not know runbooks, records or sessions"
|
|
98
|
+
recording.can_only_use :foundation,
|
|
99
|
+
because: "run records are plain data on disk, readable without a live session"
|
|
100
|
+
session.can_only_use :foundation, :document, :execution, :recording,
|
|
101
|
+
because: "the session sits over the model and knows nothing of HTTP or the terminal"
|
|
102
|
+
pages.can_only_use :foundation, :document, :recording, :session,
|
|
103
|
+
because: "pages render a session; they never touch processes or routes"
|
|
104
|
+
routes.can_only_use :foundation, :document, :recording, :session, :pages,
|
|
105
|
+
because: "routes talk to the session and render through Pages; execution goes through the session"
|
|
106
|
+
console.can_only_use :foundation, :document, :session,
|
|
107
|
+
because: "terminal flows run steps through the session, never the web layer"
|
|
108
|
+
cli.can_only_use :foundation, :document, :session, :pages, :routes, :console,
|
|
109
|
+
because: "the CLI wires the front ends together"
|
|
110
|
+
|
|
111
|
+
no_cycles
|
|
112
|
+
|
|
113
|
+
# ── Third-party boundaries ──────────────────────────────────────────────────
|
|
114
|
+
#
|
|
115
|
+
# Each library stays behind the component that owns it: the web stack behind
|
|
116
|
+
# routes (and Rack::Utils for escaping in pages), kramdown and rouge behind
|
|
117
|
+
# Renderer, myway_config behind Config. The model loads without Sinatra.
|
|
118
|
+
|
|
119
|
+
VENDORS = {
|
|
120
|
+
%w[Sinatra Puma Rackup] => %i[routes],
|
|
121
|
+
%w[Rack] => %i[routes pages],
|
|
122
|
+
%w[Kramdown Rouge] => %i[document],
|
|
123
|
+
%w[MywayConfig Anyway] => %i[foundation]
|
|
124
|
+
}.freeze
|
|
125
|
+
|
|
126
|
+
vendor_reason = "third-party libraries are reached only through the component that owns them"
|
|
127
|
+
%i[foundation document execution recording session pages routes console cli].each do |layer|
|
|
128
|
+
forbidden = VENDORS.reject { |_, owners| owners.include?(layer) }.keys.flatten
|
|
129
|
+
send(layer).cannot_reference_constants(*forbidden, because: vendor_reason)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# ── Process control ─────────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
spawn_reason = "only Executor spawns runbook code, so every execution is recorded and redacted"
|
|
135
|
+
%i[foundation document recording session pages routes console].each do |layer|
|
|
136
|
+
send(layer).cannot_call(:spawn, :exec, :system, :popen, :popen3, :capture2, :capture3, because: spawn_reason)
|
|
137
|
+
end
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,40 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## [Unreleased
|
|
3
|
+
## [0.0.2] - Unreleased
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `runsheets --new [DIR]` asks what kind of runbook to make (a directory or a
|
|
8
|
+
single file, a template, a name, a title, when to use it and who to call)
|
|
9
|
+
and writes it into `DIR`. Enter accepts each default, and answers can be
|
|
10
|
+
piped in. Without `DIR` it asks for one, offering the `dir:` a config file
|
|
11
|
+
sets, else `RUNSHEETS_DIR`.
|
|
12
|
+
- Runbook templates in `templates/`, a ladder of structure: `checks` (checks
|
|
13
|
+
only), `tasks` (checks and tasks, the default), `rollback`, `timeouts`,
|
|
14
|
+
`inputs` and `asgard` (tasks in `.loki` files beside the runbook). Each is
|
|
15
|
+
written once as a directory runbook and produces either shape; a single
|
|
16
|
+
file from a template with support files gets a folder of its own.
|
|
17
|
+
- `runsheets --run [STEP] RUNBOOK` runs one step at the terminal (listing the
|
|
18
|
+
steps to choose from when `STEP` is left out), recorded in a session like
|
|
19
|
+
a run from the page, and exits non-zero if the step fails.
|
|
20
|
+
`--engineer` and `--why` are required. It asks for the runbook's inputs
|
|
21
|
+
first, Enter keeping each one's value. A destructive block still needs
|
|
22
|
+
its confirmation code typed, a terminal block or manual step asks to be
|
|
23
|
+
confirmed, and a step with a background block is refused. Name a step by
|
|
24
|
+
its name or slug, or `verify`, `rollback` or `runbook`; `--run verify` is
|
|
25
|
+
a health check. `STEP` is never a number: numbers are only places in the
|
|
26
|
+
list shown when `STEP` is left out. Exits 0 on success, 1 on failure and
|
|
27
|
+
130 on Ctrl-C.
|
|
28
|
+
- A relative `RUNBOOK` not found from the current directory is looked for
|
|
29
|
+
under the `dir:` a config file sets, else `RUNSHEETS_DIR`.
|
|
30
|
+
- `--check` warns about a numbered step named like `verify.md`,
|
|
31
|
+
`rollback.md` or the landing page (`030-verify`), since `--run verify`
|
|
32
|
+
runs `verify.md`.
|
|
33
|
+
|
|
34
|
+
### Removed (breaking)
|
|
35
|
+
|
|
36
|
+
- `--init`, `--no-init`, `RUNSHEETS_INIT` and the `init` setting. Use
|
|
37
|
+
`--new`; the `tasks` template is the nearest to what `--init` wrote.
|
|
4
38
|
|
|
5
39
|
## [0.0.1] - 2026-10-11
|
|
6
40
|
|
data/README.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/MadBomber/runsheets/main/docs/assets/images/logo.png" alt="runsheets logo: a smiling runsheet sprinting beside the runsheets wordmark" width="620">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
1
5
|
# runsheets
|
|
2
6
|
|
|
3
7
|
> **Note:** This is a proof-of-concept and a work in progress. Features,
|
|
@@ -47,7 +51,7 @@ runsheets path/to/runbook.md # serve a single all-in-one markdown file
|
|
|
47
51
|
runsheets path/to/runbooks # a directory of runbooks: choose one in the browser
|
|
48
52
|
runsheets --open path/to/runbook # and open the browser
|
|
49
53
|
runsheets --check path/to/runbook # load it, print authoring warnings, exit
|
|
50
|
-
runsheets --
|
|
54
|
+
runsheets --new path/to/runbooks # answer a few questions, get a runbook from a template
|
|
51
55
|
```
|
|
52
56
|
|
|
53
57
|
The argument is one of three things: a runbook directory, a single
|
|
@@ -76,11 +80,11 @@ runsheets --open examples/single_md_file_runbooks/disk-space-triage.md # links t
|
|
|
76
80
|
|
|
77
81
|
Options: `--port`, `--bind` (default loopback), `--runs-dir` (where run
|
|
78
82
|
records go; default `~/.local/share/runsheets/runs`), `--open`, `--check`,
|
|
79
|
-
`--
|
|
83
|
+
`--new [DIR]`, `--run [STEP]` to run one step at the terminal (by its name or slug), `--config FILE`, `--dump` to print the settings in force as a
|
|
80
84
|
config file (redirect it to save them), and for the session `--engineer`,
|
|
81
85
|
`--why`, `--log-level`, `--verbose` and `--quiet`. Settings are layered: the command line beats
|
|
82
86
|
`RUNSHEETS_*` environment variables (`RUNSHEETS_PORT`, `RUNSHEETS_DIR` for
|
|
83
|
-
the runbook, ...), which beat `./config/runsheets.yml` (or the file `--config` or
|
|
87
|
+
the runbook, under which a relative `RUNBOOK` is also looked for, ...), which beat `./config/runsheets.yml` (or the file `--config` or
|
|
84
88
|
`RUNSHEETS_CONFIG` names), which beats `~/.config/runsheets/runsheets.yml`,
|
|
85
89
|
which beats the defaults bundled in `lib/runsheets/config/defaults.yml`. See
|
|
86
90
|
[docs/running/cli.md](docs/running/cli.md#settings).
|
data/archspec_todo.yml
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
violations:
|
|
3
|
+
- id: bd6f748ea23f791211b2342d
|
|
4
|
+
rule: dependencies.no_cycles
|
|
5
|
+
path: lib/runsheets/cli.rb
|
|
6
|
+
line: 322
|
|
7
|
+
message: 'component dependency cycle: cli -> console -> cli'
|
|
8
|
+
- id: 4af0bc5913dc3dfbdd2512bc
|
|
9
|
+
rule: dependencies.allow
|
|
10
|
+
path: lib/runsheets/pages.rb
|
|
11
|
+
line: 648
|
|
12
|
+
message: pages may not depend on routes
|
|
13
|
+
- id: 542009c67c4ebffeb4d329f7
|
|
14
|
+
rule: dependencies.no_cycles
|
|
15
|
+
path: lib/runsheets/pages.rb
|
|
16
|
+
line: 648
|
|
17
|
+
message: 'component dependency cycle: pages -> routes -> pages'
|
|
18
|
+
- id: 4af0bc5913dc3dfbdd2512bc
|
|
19
|
+
rule: dependencies.allow
|
|
20
|
+
path: lib/runsheets/pages.rb
|
|
21
|
+
line: 651
|
|
22
|
+
message: pages may not depend on routes
|
|
23
|
+
- id: 5521b0d9be73ab02461a4abe
|
|
24
|
+
rule: dependencies.allow
|
|
25
|
+
path: lib/runsheets/wizard.rb
|
|
26
|
+
line: 237
|
|
27
|
+
message: console may not depend on cli
|
|
28
|
+
- id: 5521b0d9be73ab02461a4abe
|
|
29
|
+
rule: dependencies.allow
|
|
30
|
+
path: lib/runsheets/wizard.rb
|
|
31
|
+
line: 238
|
|
32
|
+
message: console may not depend on cli
|
data/lib/runsheets/cli.rb
CHANGED
|
@@ -6,7 +6,7 @@ require "socket"
|
|
|
6
6
|
|
|
7
7
|
module Runsheets
|
|
8
8
|
# The `runsheets` command: load a runbook (a directory or a single markdown
|
|
9
|
-
# file) and serve it
|
|
9
|
+
# file) and serve it or check it; or ask some questions and write a new one.
|
|
10
10
|
#
|
|
11
11
|
# Settings come from Config: bundled defaults, then the config file, then
|
|
12
12
|
# RUNSHEETS_* variables, then whatever is given on the command line.
|
|
@@ -22,7 +22,7 @@ module Runsheets
|
|
|
22
22
|
# names for the options, :config for the config file. Raises
|
|
23
23
|
# OptionParser::ParseError on bad input; returns nil after printing help
|
|
24
24
|
# or the version.
|
|
25
|
-
def self.parse(argv, out: $stdout)
|
|
25
|
+
def self.parse(argv, out: $stdout, cwd: Dir.pwd)
|
|
26
26
|
options = {}
|
|
27
27
|
defaults = Config.bundled_defaults
|
|
28
28
|
parser = OptionParser.new do |opts|
|
|
@@ -30,6 +30,8 @@ module Runsheets
|
|
|
30
30
|
#{PROGRAM} #{VERSION} - executable runbooks in your browser
|
|
31
31
|
|
|
32
32
|
Usage: #{PROGRAM} [options] [RUNBOOK]
|
|
33
|
+
#{PROGRAM} --new [DIR]
|
|
34
|
+
#{PROGRAM} --run [STEP] --engineer NAME --why TEXT [RUNBOOK]
|
|
33
35
|
|
|
34
36
|
RUNBOOK is a directory holding runbook.md and a steps/ directory of
|
|
35
37
|
markdown files, a single all-in-one markdown file whose ## headings
|
|
@@ -44,8 +46,8 @@ module Runsheets
|
|
|
44
46
|
then ./config/runsheets.yml (or the file --config names), then the
|
|
45
47
|
environment variable named beside each option, then the command line,
|
|
46
48
|
which wins. RUNBOOK itself can be set as `dir:` in a config file or
|
|
47
|
-
with RUNSHEETS_DIR. Flags take 1, true, yes or on; --no-open
|
|
48
|
-
--no-check
|
|
49
|
+
with RUNSHEETS_DIR. Flags take 1, true, yes or on; --no-open and
|
|
50
|
+
--no-check switch a flag off that a lower layer turned on.
|
|
49
51
|
|
|
50
52
|
Options:
|
|
51
53
|
BANNER
|
|
@@ -64,11 +66,28 @@ module Runsheets
|
|
|
64
66
|
|
|
65
67
|
rest = parser.parse(argv)
|
|
66
68
|
raise OptionParser::ParseError, "too many arguments: #{rest.join(' ')}" if rest.size > 1
|
|
69
|
+
raise OptionParser::ParseError, "--new takes the directory to create the runbook in; give no RUNBOOK with it" if options.key?(:new) && rest.any?
|
|
70
|
+
raise OptionParser::ParseError, "--run and --new cannot be used together" if options.key?(:new) && options.key?(:run)
|
|
67
71
|
|
|
72
|
+
rest = runbook_taken_by_run(options, rest, cwd:)
|
|
68
73
|
options[:dir] = rest.first if rest.first
|
|
69
74
|
options
|
|
70
75
|
end
|
|
71
76
|
|
|
77
|
+
# --run's step is optional, so in `--run db-refresh` it takes the
|
|
78
|
+
# runbook as the step. A value with a / or ending in .md, or that names
|
|
79
|
+
# something in +cwd+, is no step name: when nothing else was given as
|
|
80
|
+
# RUNBOOK it is moved there, and the step is asked for. Returns the
|
|
81
|
+
# arguments left over.
|
|
82
|
+
def self.runbook_taken_by_run(options, rest, cwd: Dir.pwd)
|
|
83
|
+
value = options[:run]
|
|
84
|
+
return rest unless rest.empty? && value
|
|
85
|
+
return rest unless value.include?("/") || value.end_with?(".md") || File.exist?(File.join(cwd, value))
|
|
86
|
+
|
|
87
|
+
options[:run] = nil
|
|
88
|
+
[value]
|
|
89
|
+
end
|
|
90
|
+
|
|
72
91
|
# The options about serving: config file, port, bind, records, and the
|
|
73
92
|
# one-shot actions.
|
|
74
93
|
def self.server_options(opts, options, defaults)
|
|
@@ -78,7 +97,9 @@ module Runsheets
|
|
|
78
97
|
opts.on("--runs-dir DIR", "Where run records are written (default: #{defaults[:runs_dir]}) [RUNSHEETS_RUNS_DIR]") { options[:runs_dir] = it }
|
|
79
98
|
opts.on("-o", "--[no-]open", "Open the browser once the server is up [RUNSHEETS_OPEN]") { options[:open] = it }
|
|
80
99
|
opts.on("--[no-]check", "Load the runbook, report authoring warnings, and exit [RUNSHEETS_CHECK]") { options[:check] = it }
|
|
81
|
-
opts.on("--[
|
|
100
|
+
opts.on("--new [DIR]", "Ask what kind of runbook to make, write it from a template into DIR (asked for when left out), and exit") { options[:new] = it }
|
|
101
|
+
opts.on("--run [STEP]", "Run one step of RUNBOOK at the terminal, named by its slug or name (or verify, rollback, runbook); " \
|
|
102
|
+
"left out, choose it by number from a list. Needs -e and -w") { options[:run] = it }
|
|
82
103
|
opts.on("--[no-]dump", "Print the settings in force as a config file to stdout and exit [RUNSHEETS_DUMP]") { options[:dump] = it }
|
|
83
104
|
end
|
|
84
105
|
|
|
@@ -117,27 +138,37 @@ module Runsheets
|
|
|
117
138
|
0
|
|
118
139
|
end
|
|
119
140
|
|
|
120
|
-
# The
|
|
121
|
-
#
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
141
|
+
# The RUNBOOK argument as a path. A relative one is found from the
|
|
142
|
+
# current directory, else under +base+: the dir RUNSHEETS_DIR or a
|
|
143
|
+
# config file sets (or the directory of a single-file runbook it
|
|
144
|
+
# names). Raises RunbookError naming both places when neither has it.
|
|
145
|
+
def self.resolve_runbook(arg, base:, cwd: Dir.pwd)
|
|
146
|
+
here = Config.expand_path(arg, cwd)
|
|
147
|
+
return here if File.exist?(here) || base.nil? || Pathname.new(arg).absolute? || arg.start_with?("~")
|
|
126
148
|
|
|
127
|
-
|
|
149
|
+
base = File.dirname(base) if File.file?(base)
|
|
150
|
+
under = Config.expand_path(arg, base)
|
|
151
|
+
return under if File.exist?(under)
|
|
152
|
+
|
|
153
|
+
raise RunbookError, "no such runbook: #{here}, nor #{under} under the runbook directory #{base}"
|
|
128
154
|
end
|
|
129
155
|
|
|
156
|
+
# The runbook a configuration points at: its dir, else the bundled example.
|
|
157
|
+
def self.runbook_path(config) = config.dir || DEFAULT_RUNBOOK
|
|
158
|
+
|
|
130
159
|
# Entry point for the executable. Returns the process exit status.
|
|
131
|
-
|
|
160
|
+
# --new and --run read their answers from +input+.
|
|
161
|
+
def self.run(argv, out: $stdout, err: $stderr, input: $stdin)
|
|
132
162
|
options = parse(argv, out:)
|
|
133
163
|
return 0 unless options
|
|
164
|
+
return new_runbook(options[:new], default: configured_runbook_dir(offline_config(options)), input:, out:) if options.key?(:new)
|
|
134
165
|
|
|
135
|
-
config
|
|
166
|
+
config = options.key?(:run) ? offline_config(options) : configure(options)
|
|
167
|
+
config.dir = resolve_runbook(options[:dir], base: configured_runbook_dir(config)) if options[:dir]
|
|
168
|
+
return run_step(options[:run], config, input:, out:) if options.key?(:run)
|
|
136
169
|
return dump(config, out:) if config.dump
|
|
137
170
|
|
|
138
|
-
path
|
|
139
|
-
return init(path, out:) if config.init
|
|
140
|
-
|
|
171
|
+
path = runbook_path(config)
|
|
141
172
|
target = Library.library?(path) ? Library.load(path) : Runbook.load(path)
|
|
142
173
|
return check(target, out:, err:) if config.check
|
|
143
174
|
|
|
@@ -146,7 +177,7 @@ module Runsheets
|
|
|
146
177
|
rescue Errno::EADDRINUSE
|
|
147
178
|
err.puts "#{PROGRAM}: port #{config.port} on #{config.bind} is already in use; pick another with --port"
|
|
148
179
|
1
|
|
149
|
-
rescue OptionParser::ParseError, RunbookError, ConfigError,
|
|
180
|
+
rescue OptionParser::ParseError, RunbookError, RunError, ConfigError,
|
|
150
181
|
Psych::Exception, SocketError, SystemCallError => e # also a bad config file, an unknown host, an unreadable directory
|
|
151
182
|
err.puts "#{PROGRAM}: #{e.message}"
|
|
152
183
|
1
|
|
@@ -225,7 +256,9 @@ module Runsheets
|
|
|
225
256
|
|
|
226
257
|
# Who the start page suggests: the configured engineer, else git's
|
|
227
258
|
# user.name, else the login name.
|
|
228
|
-
def self.default_engineer(config
|
|
259
|
+
def self.default_engineer(config, env: ENV, git_name: git_user_name)
|
|
260
|
+
[config.engineer, git_name, env["USER"]].find { !it.to_s.strip.empty? }
|
|
261
|
+
end
|
|
229
262
|
|
|
230
263
|
def self.git_user_name
|
|
231
264
|
IO.popen(%w[git config user.name], err: File::NULL, &:read).strip
|
|
@@ -267,158 +300,59 @@ module Runsheets
|
|
|
267
300
|
nil
|
|
268
301
|
end
|
|
269
302
|
|
|
270
|
-
#
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
#
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
File.write(full, text)
|
|
284
|
-
out.puts "created #{full}"
|
|
285
|
-
end
|
|
286
|
-
out.puts "Next: #{PROGRAM} --check #{path}"
|
|
287
|
-
0
|
|
303
|
+
# The settings for --new and --run. Nothing is served, so a project
|
|
304
|
+
# config that binds to the network is no concern.
|
|
305
|
+
def self.offline_config(options) = Runsheets.configure(options.except(:config, :new, :run), path: options[:config])
|
|
306
|
+
|
|
307
|
+
# The runbook directory the settings name, apart from the command line:
|
|
308
|
+
# the dir a config file sets, else $RUNSHEETS_DIR, else nil. A dir
|
|
309
|
+
# naming a single-file runbook gives the directory it is in. --new
|
|
310
|
+
# offers it when given no directory, and a relative RUNBOOK not found
|
|
311
|
+
# from the current directory is looked for under it.
|
|
312
|
+
def self.configured_runbook_dir(config, env: ENV)
|
|
313
|
+
dir = config.file_dir
|
|
314
|
+
dir ||= Config.expand_path(env["RUNSHEETS_DIR"].strip) unless Config.blank?(env["RUNSHEETS_DIR"])
|
|
315
|
+
dir && File.file?(dir) ? File.dirname(dir) : dir
|
|
288
316
|
end
|
|
289
317
|
|
|
290
|
-
#
|
|
291
|
-
#
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
{
|
|
299
|
-
"runbook.md" => runbook_template(title),
|
|
300
|
-
"steps/010-first-step.md" => <<~MD,
|
|
301
|
-
---
|
|
302
|
-
title: First step
|
|
303
|
-
kind: automated
|
|
304
|
-
timeout: 300
|
|
305
|
-
---
|
|
306
|
-
|
|
307
|
-
Say what this step does and why it is here. Then the command:
|
|
308
|
-
|
|
309
|
-
```bash run
|
|
310
|
-
echo "hello from #{name}"
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
How to tell it worked:
|
|
314
|
-
|
|
315
|
-
```text expect
|
|
316
|
-
hello from #{name}
|
|
317
|
-
```
|
|
318
|
-
MD
|
|
319
|
-
"steps/020-a-manual-step.md" => <<~MD,
|
|
320
|
-
---
|
|
321
|
-
title: A manual step
|
|
322
|
-
kind: manual
|
|
323
|
-
---
|
|
324
|
-
|
|
325
|
-
An instruction the operator carries out and then acknowledges.
|
|
326
|
-
MD
|
|
327
|
-
"verify.md" => <<~MD,
|
|
328
|
-
---
|
|
329
|
-
title: Verify
|
|
330
|
-
kind: verify
|
|
331
|
-
---
|
|
332
|
-
|
|
333
|
-
Whole-procedure checks. These should pass after every run.
|
|
334
|
-
|
|
335
|
-
```bash run
|
|
336
|
-
test -n "$EXAMPLE_INPUT" && echo "EXAMPLE_INPUT is set"
|
|
337
|
-
```
|
|
338
|
-
MD
|
|
339
|
-
"rollback.md" => <<~MD
|
|
340
|
-
---
|
|
341
|
-
title: Rollback
|
|
342
|
-
---
|
|
343
|
-
|
|
344
|
-
What to do when it fails partway. This panel sits in the sidebar of
|
|
345
|
-
every step.
|
|
346
|
-
MD
|
|
347
|
-
}
|
|
348
|
-
end
|
|
318
|
+
# --new: ask what kind of runbook to make and write it into +dir+; with
|
|
319
|
+
# no dir, ask for that too, offering +default+. Ctrl-C at a question
|
|
320
|
+
# creates nothing. Returns the exit status.
|
|
321
|
+
def self.new_runbook(dir, default: nil, input: $stdin, out: $stdout)
|
|
322
|
+
Wizard.new(dir, input:, output: out, default_parent: default).run
|
|
323
|
+
rescue Interrupt
|
|
324
|
+
out.puts "", Wizard::CANCELLED
|
|
325
|
+
130
|
|
349
326
|
end
|
|
350
327
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
MD
|
|
328
|
+
# --run: run one step of the configured runbook at the terminal, in a
|
|
329
|
+
# session of its own that ends when the step does. The engineer and why
|
|
330
|
+
# are required, since nobody is at a start page to give them. Ctrl-C
|
|
331
|
+
# stops what is running. Returns the exit status.
|
|
332
|
+
def self.run_step(name, config, input: $stdin, out: $stdout)
|
|
333
|
+
raise ConfigError, "--run needs --engineer and --why: the run is recorded with who ran it and why" unless config.engineer && config.why
|
|
334
|
+
|
|
335
|
+
runbook = run_step_runbook(runbook_path(config))
|
|
336
|
+
step = name ? StepRunner.find_step(runbook, name) : StepRunner.choose_step(runbook, input:, output: out)
|
|
337
|
+
runner = StepRunner.new(runbook:, step:, input:, output: out)
|
|
338
|
+
session = Session.new(engineer: config.engineer, why: config.why, runbook:, **session_options(config, out:))
|
|
339
|
+
status = runner.run(session)
|
|
340
|
+
ending = status.zero? ? "succeeded" : "failed"
|
|
341
|
+
status
|
|
342
|
+
rescue Interrupt
|
|
343
|
+
ending = "was interrupted"
|
|
344
|
+
out.puts "", "Interrupted."
|
|
345
|
+
130
|
|
346
|
+
ensure
|
|
347
|
+
session&.end!("runsheets --run #{step.slug} #{ending || 'stopped on an error'}")
|
|
372
348
|
end
|
|
373
349
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
when_to_use: >
|
|
379
|
-
One or two sentences on the situation this runbook is for.
|
|
380
|
-
prerequisites:
|
|
381
|
-
- What must be true or installed before starting
|
|
382
|
-
escalation: Who to call when a step fails
|
|
383
|
-
inputs:
|
|
384
|
-
- name: EXAMPLE_INPUT
|
|
385
|
-
prompt: An example input, exported as $EXAMPLE_INPUT
|
|
386
|
-
default: example
|
|
387
|
-
---
|
|
388
|
-
|
|
389
|
-
# #{title}
|
|
390
|
-
|
|
391
|
-
Everything above the first `##` heading is the preamble. Each `##` heading
|
|
392
|
-
below is a step. Step attributes go in an HTML comment right after the
|
|
393
|
-
heading.
|
|
394
|
-
|
|
395
|
-
## First step
|
|
396
|
-
<!-- kind: automated, timeout: 300 -->
|
|
397
|
-
|
|
398
|
-
Say what this step does and why it is here. Then the command:
|
|
399
|
-
|
|
400
|
-
```bash run
|
|
401
|
-
echo "hello from $EXAMPLE_INPUT"
|
|
402
|
-
```
|
|
403
|
-
|
|
404
|
-
## A manual step
|
|
405
|
-
<!-- kind: manual -->
|
|
406
|
-
|
|
407
|
-
An instruction the operator carries out and then acknowledges.
|
|
408
|
-
|
|
409
|
-
## Verify
|
|
410
|
-
|
|
411
|
-
Whole-procedure checks. A section headed Verify plays the part of verify.md.
|
|
412
|
-
|
|
413
|
-
```bash run
|
|
414
|
-
test -n "$EXAMPLE_INPUT" && echo "EXAMPLE_INPUT is set"
|
|
415
|
-
```
|
|
416
|
-
|
|
417
|
-
## Rollback
|
|
350
|
+
# The runbook at +path+ for --run, which runs a step of one runbook and
|
|
351
|
+
# so cannot be given a library.
|
|
352
|
+
def self.run_step_runbook(path)
|
|
353
|
+
raise RunbookError, "--run needs a runbook, not a library of them: #{path}" if Library.library?(path)
|
|
418
354
|
|
|
419
|
-
|
|
420
|
-
sidebar of every step.
|
|
421
|
-
MD
|
|
355
|
+
Runbook.load(path)
|
|
422
356
|
end
|
|
423
357
|
end
|
|
424
358
|
end
|
|
@@ -21,8 +21,6 @@ defaults:
|
|
|
21
21
|
open: false
|
|
22
22
|
# Load the runbook, report authoring warnings, and exit. (--check, RUNSHEETS_CHECK)
|
|
23
23
|
check: false
|
|
24
|
-
# Create a runbook skeleton at dir and exit. (--init, RUNSHEETS_INIT)
|
|
25
|
-
init: false
|
|
26
24
|
# Print the settings in force as a config file to stdout and exit. (--dump, RUNSHEETS_DUMP)
|
|
27
25
|
dump: false
|
|
28
26
|
# Who is starting the session. Empty means ask, prefilled from
|