runsheets 0.0.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 +7 -0
- data/CHANGELOG.md +98 -0
- data/LICENSE.txt +21 -0
- data/README.md +277 -0
- data/bin/runsheets +12 -0
- data/examples/about-the-examples.md +40 -0
- data/examples/directory_based_runbooks/hello/rollback.md +11 -0
- data/examples/directory_based_runbooks/hello/runbook.md +40 -0
- data/examples/directory_based_runbooks/hello/steps/010-say-hello.md +33 -0
- data/examples/directory_based_runbooks/hello/steps/020-inspect-ruby.md +28 -0
- data/examples/directory_based_runbooks/hello/steps/030-take-a-breath.md +11 -0
- data/examples/directory_based_runbooks/hello/steps/035-keep-a-clock-running.md +21 -0
- data/examples/directory_based_runbooks/hello/steps/040-exercise-failure.md +26 -0
- data/examples/directory_based_runbooks/hello/steps/045-check-the-greeting.md +18 -0
- data/examples/directory_based_runbooks/hello/verify.md +18 -0
- data/examples/directory_based_runbooks/staging-teardown/rollback.md +28 -0
- data/examples/directory_based_runbooks/staging-teardown/runbook.md +57 -0
- data/examples/directory_based_runbooks/staging-teardown/steps/010-confirm-nothing-to-preserve.md +18 -0
- data/examples/directory_based_runbooks/staging-teardown/steps/020-snapshot-the-database.md +47 -0
- data/examples/directory_based_runbooks/staging-teardown/steps/030-stop-the-pipeline.md +32 -0
- data/examples/directory_based_runbooks/staging-teardown/steps/040-drain-the-ecs-services.md +35 -0
- data/examples/directory_based_runbooks/staging-teardown/steps/050-record-the-final-row-counts.md +41 -0
- data/examples/directory_based_runbooks/staging-teardown/steps/060-delete-the-stack.md +56 -0
- data/examples/directory_based_runbooks/staging-teardown/verify.md +40 -0
- data/examples/single_md_file_runbooks/db-maintenance.md +163 -0
- data/examples/single_md_file_runbooks/disk-space-triage.md +90 -0
- data/examples/single_md_file_runbooks/disk-usage-glossary.md +48 -0
- data/examples/single_md_file_runbooks/policies/cleanup-policy.md +33 -0
- data/examples/single_md_file_runbooks/policies/images/triage-flow.svg +44 -0
- data/examples/single_md_file_runbooks/runbooks_with_scripts/.loki +17 -0
- data/examples/single_md_file_runbooks/runbooks_with_scripts/asgard-tasks.md +135 -0
- data/examples/single_md_file_runbooks/runbooks_with_scripts/checks.loki +27 -0
- data/examples/single_md_file_runbooks/runbooks_with_scripts/greet.loki +6 -0
- data/lib/runsheets/assets.rb +780 -0
- data/lib/runsheets/block.rb +118 -0
- data/lib/runsheets/cli.rb +424 -0
- data/lib/runsheets/config/defaults.yml +44 -0
- data/lib/runsheets/config.rb +139 -0
- data/lib/runsheets/diff.rb +67 -0
- data/lib/runsheets/execution.rb +144 -0
- data/lib/runsheets/executor.rb +157 -0
- data/lib/runsheets/expectation.rb +101 -0
- data/lib/runsheets/fences.rb +172 -0
- data/lib/runsheets/front_matter.rb +29 -0
- data/lib/runsheets/library.rb +268 -0
- data/lib/runsheets/pages/chooser.rb +408 -0
- data/lib/runsheets/pages/search.rb +119 -0
- data/lib/runsheets/pages/tree.rb +88 -0
- data/lib/runsheets/pages.rb +947 -0
- data/lib/runsheets/redactor.rb +88 -0
- data/lib/runsheets/renderer.rb +229 -0
- data/lib/runsheets/run.rb +330 -0
- data/lib/runsheets/run_record.rb +371 -0
- data/lib/runsheets/runbook.rb +301 -0
- data/lib/runsheets/search.rb +129 -0
- data/lib/runsheets/session.rb +307 -0
- data/lib/runsheets/session_log.rb +114 -0
- data/lib/runsheets/single_file.rb +93 -0
- data/lib/runsheets/step.rb +91 -0
- data/lib/runsheets/version.rb +5 -0
- data/lib/runsheets/web.rb +537 -0
- data/lib/runsheets.rb +90 -0
- data/sig/runsheets.rbs +4 -0
- metadata +226 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 5b4caf43ab35c8816f90d794e6dba06e3fd3644cd838c89c9987bbe0f60ab786
|
|
4
|
+
data.tar.gz: 39cd9610b55e341bc1b664443a509a2269f7397c190e28f38fcbcc6c9287f2db
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: cec8d91aa87096dfae4548d1eb39df3c9b6d57e0d2176d24e12550f9ff7a21c0487e0f850b7fd0c5fd9178f7cd471057e40771c3a2918b897ec7f57fa63b1282
|
|
7
|
+
data.tar.gz: 2d960f098334bf2b7900eae5084c1fa722a27b6c115d6da26e4020e4cd9b5c32a546d265ba448a36d4bbb6dfcfdb730ae16d7ab0b3112ae6308d341f54c16ba0
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
## [0.0.1] - 2026-10-11
|
|
6
|
+
|
|
7
|
+
First release. runsheets turns a markdown runbook into a local web page
|
|
8
|
+
where marked commands get a Run button, and records everything that
|
|
9
|
+
happens during a run as a *runsheet*.
|
|
10
|
+
|
|
11
|
+
### Runbooks
|
|
12
|
+
|
|
13
|
+
- A runbook is a directory (`runbook.md` with YAML front matter, ordered
|
|
14
|
+
`steps/`, optional `verify.md` and `rollback.md`) or a single markdown
|
|
15
|
+
file whose `##` headings are the steps.
|
|
16
|
+
- Front matter declares `inputs` (with secrets) and `interpreters` that map
|
|
17
|
+
other languages, such as `sql` through `psql`, to a command.
|
|
18
|
+
- `runsheets --init PATH` scaffolds a starter runbook; `runsheets --check`
|
|
19
|
+
reports authoring warnings.
|
|
20
|
+
- Runbooks are re-read when their files change on disk.
|
|
21
|
+
|
|
22
|
+
### The fenced block convention
|
|
23
|
+
|
|
24
|
+
- The info string marks a block as `run`, `destructive`, `background`,
|
|
25
|
+
`terminal` or `expect`.
|
|
26
|
+
- `run` blocks execute `bash`, `sh`, `zsh`, `ruby` or any mapped language
|
|
27
|
+
as a fresh process group with a per-step timeout.
|
|
28
|
+
- `destructive` blocks need a typed confirmation, checked on the server.
|
|
29
|
+
- `background` blocks get Start and Stop buttons and appear in a Running
|
|
30
|
+
panel on every page.
|
|
31
|
+
- `terminal` blocks are run by the operator, who confirms with "I ran this"
|
|
32
|
+
and an optional note.
|
|
33
|
+
- `text expect` blocks are compared with the output of the block above
|
|
34
|
+
them. `{{any}}` matches text that changes on each run, and a mismatch
|
|
35
|
+
shows the output and the expected text side by side with the differences
|
|
36
|
+
highlighted.
|
|
37
|
+
|
|
38
|
+
### Libraries, search and documents
|
|
39
|
+
|
|
40
|
+
- `runsheets DIR` on a folder of runbooks opens a library: a folder tree
|
|
41
|
+
with a filter box, each folder's `README.md`, and a summary of each
|
|
42
|
+
runbook with its inputs, steps and previous runs.
|
|
43
|
+
- Full-text search across every runbook in the library, with ranked
|
|
44
|
+
results and highlighted snippets.
|
|
45
|
+
- Relative links to other markdown files open as pages: another runbook,
|
|
46
|
+
or a plain document with nothing executable.
|
|
47
|
+
- A Checks page gathers every verify document and runs them all with one
|
|
48
|
+
button.
|
|
49
|
+
|
|
50
|
+
### Sessions and the run record
|
|
51
|
+
|
|
52
|
+
- Everything between starting and stopping runsheets is one session, with
|
|
53
|
+
an engineer and a reason (from the start page, or `--engineer` and
|
|
54
|
+
`--why`).
|
|
55
|
+
- The session page holds timestamped notes, every run's progress, the tail
|
|
56
|
+
of the session log, and an End session button. Ending a session, or
|
|
57
|
+
Ctrl-C, closes each run as `completed`, `partial` or `opened`. A session
|
|
58
|
+
left open by a killed process is closed as `interrupted` at the next
|
|
59
|
+
start.
|
|
60
|
+
- Each run records the exact command, output, exit status, timing, step
|
|
61
|
+
acknowledgements and notes in `run.json` and a readable `run.md`.
|
|
62
|
+
- `session.log` records every action and line of output, timestamped and
|
|
63
|
+
levelled, and echoes it to the terminal.
|
|
64
|
+
- Run history, and a diff for every block whose code has changed since a
|
|
65
|
+
past run.
|
|
66
|
+
|
|
67
|
+
### Security
|
|
68
|
+
|
|
69
|
+
- Binds to loopback by default. Every request needs the session key the
|
|
70
|
+
printed `?token=` link gives the browser as a cookie, so other accounts
|
|
71
|
+
on the machine see nothing; every state-changing request also needs the
|
|
72
|
+
per-process session token and an allowed Host header.
|
|
73
|
+
- Secret inputs are redacted from output and logs, however the output is
|
|
74
|
+
split, and never written back into the page.
|
|
75
|
+
- Every page has a Content-Security-Policy with a per-response nonce, so a
|
|
76
|
+
`<script>` in a runbook's markdown does not run, and the page's script
|
|
77
|
+
acts only on blocks runsheets rendered, so a copied Run button does
|
|
78
|
+
nothing. Run refuses a block edited since the page loaded. Files served
|
|
79
|
+
from a runbook are sandboxed and cannot follow a symlink out of it.
|
|
80
|
+
- Anything a block leaves running in its process group is stopped when
|
|
81
|
+
the block finishes.
|
|
82
|
+
- A project `./config/runsheets.yml` cannot by itself bind runsheets to a
|
|
83
|
+
non-loopback address.
|
|
84
|
+
|
|
85
|
+
### Settings
|
|
86
|
+
|
|
87
|
+
- Layered settings through `myway_config`. Command-line flags override
|
|
88
|
+
`RUNSHEETS_*` environment variables, which override `./config/runsheets.yml`,
|
|
89
|
+
which overrides `~/.config/runsheets/runsheets.yml`, which overrides the
|
|
90
|
+
bundled defaults.
|
|
91
|
+
- `runsheets --dump` prints the settings in force as a config file.
|
|
92
|
+
|
|
93
|
+
### Examples
|
|
94
|
+
|
|
95
|
+
- `examples/` holds directory and single-file runbooks: `hello`,
|
|
96
|
+
`staging-teardown`, `db-maintenance.md`, `disk-space-triage.md` with the
|
|
97
|
+
plain documents it links to, and `asgard-tasks.md`, whose blocks call
|
|
98
|
+
asgard tasks.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dewayne VanHoozer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# runsheets
|
|
2
|
+
|
|
3
|
+
> **Note:** This is a proof-of-concept and a work in progress. Features,
|
|
4
|
+
> file formats, and the CLI may change without notice.
|
|
5
|
+
>
|
|
6
|
+
> Runbooks are common among DevOps engineers but a little foreign to me.
|
|
7
|
+
> Please [open an issue](https://github.com/MadBomber/runsheets/issues) and
|
|
8
|
+
> let me know where I get this concept wrong. I appreciate all feedback and
|
|
9
|
+
> contributions.
|
|
10
|
+
>
|
|
11
|
+
> See the [CHANGELOG.md](https://github.com/MadBomber/runsheets/blob/main/CHANGELOG.md)
|
|
12
|
+
> file for the latest changes.
|
|
13
|
+
|
|
14
|
+
**Full documentation:** <https://madbomber.github.io/runsheets>. This
|
|
15
|
+
README is the overview; the documentation site covers authoring, the
|
|
16
|
+
block convention, the web UI, the run record, settings and the security
|
|
17
|
+
model in detail.
|
|
18
|
+
|
|
19
|
+
Executable runbooks. A runbook (a directory of markdown files, or a single
|
|
20
|
+
markdown file) becomes a local web page
|
|
21
|
+
where the steps read like a doc site, the commands the author marked as
|
|
22
|
+
runnable get a Run button, and everything that happens during a run is
|
|
23
|
+
recorded: the exact command, its output, exit status, timing, and the
|
|
24
|
+
operator's acknowledgements of the manual steps.
|
|
25
|
+
|
|
26
|
+
The rendering is the vehicle. The run record, the *runsheet*, is the point.
|
|
27
|
+
|
|
28
|
+
Every block kind in the convention below is live, a runbook can be a
|
|
29
|
+
directory or a single markdown file, and everything between starting
|
|
30
|
+
runsheets and stopping it is one *session* with its own log. Vocabulary: the document is
|
|
31
|
+
the *runbook*; the record of one runbook's run is the *runsheet*.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
gem install runsheets
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Requires Ruby 3.4 or newer.
|
|
40
|
+
|
|
41
|
+
## Run
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
runsheets # serve the bundled examples/directory_based_runbooks/hello on http://127.0.0.1:4567/
|
|
45
|
+
runsheets path/to/runbook # serve a runbook directory (runbook.md + steps/)
|
|
46
|
+
runsheets path/to/runbook.md # serve a single all-in-one markdown file
|
|
47
|
+
runsheets path/to/runbooks # a directory of runbooks: choose one in the browser
|
|
48
|
+
runsheets --open path/to/runbook # and open the browser
|
|
49
|
+
runsheets --check path/to/runbook # load it, print authoring warnings, exit
|
|
50
|
+
runsheets --init path/to/new # scaffold a runbook (a .md path makes a single file)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The argument is one of three things: a runbook directory, a single
|
|
54
|
+
all-in-one markdown file whose `##` headings are the steps (see
|
|
55
|
+
[Single-file runbooks](docs/runbooks/structure.md#single-file-runbooks)),
|
|
56
|
+
or a directory holding several of either, in folders nested as deep as
|
|
57
|
+
you like. A markdown file is a runbook only when it starts with YAML front
|
|
58
|
+
matter that has a `title`; any other markdown file is a plain document that
|
|
59
|
+
a runbook can link to (see
|
|
60
|
+
[Runbooks and Documents](docs/concepts/runbooks.md)). Started on such a library, the browser opens on a folder tree in
|
|
61
|
+
the left pane; selecting a runbook shows its description, prerequisites,
|
|
62
|
+
inputs, steps and previous runs in the main pane, with the inputs form and
|
|
63
|
+
a **Start run** button.
|
|
64
|
+
A `README.md` in a folder is shown as that folder's description. The
|
|
65
|
+
search box in the header searches the full text of every runbook. Try the
|
|
66
|
+
examples bundled with the gem:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
cd "$(dirname "$(gem which runsheets)")/.." # the installed gem's directory
|
|
70
|
+
runsheets --open examples # all five, pick one in the browser
|
|
71
|
+
runsheets --open examples/directory_based_runbooks/hello # safe to run: every block kind
|
|
72
|
+
runsheets --check examples/directory_based_runbooks/staging-teardown # a realistic AWS teardown
|
|
73
|
+
runsheets --open examples/single_md_file_runbooks/db-maintenance.md # a single all-in-one file, sql blocks via psql
|
|
74
|
+
runsheets --open examples/single_md_file_runbooks/disk-space-triage.md # links to plain documents, kept out of the tree
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Options: `--port`, `--bind` (default loopback), `--runs-dir` (where run
|
|
78
|
+
records go; default `~/.local/share/runsheets/runs`), `--open`, `--check`,
|
|
79
|
+
`--init`, `--config FILE`, `--dump` to print the settings in force as a
|
|
80
|
+
config file (redirect it to save them), and for the session `--engineer`,
|
|
81
|
+
`--why`, `--log-level`, `--verbose` and `--quiet`. Settings are layered: the command line beats
|
|
82
|
+
`RUNSHEETS_*` environment variables (`RUNSHEETS_PORT`, `RUNSHEETS_DIR` for
|
|
83
|
+
the runbook, ...), which beat `./config/runsheets.yml` (or the file `--config` or
|
|
84
|
+
`RUNSHEETS_CONFIG` names), which beats `~/.config/runsheets/runsheets.yml`,
|
|
85
|
+
which beats the defaults bundled in `lib/runsheets/config/defaults.yml`. See
|
|
86
|
+
[docs/running/cli.md](docs/running/cli.md#settings).
|
|
87
|
+
|
|
88
|
+
## Sessions
|
|
89
|
+
|
|
90
|
+
Everything between starting `runsheets` and stopping it is one *session*.
|
|
91
|
+
It starts by asking who you are and why you are starting it (or take them
|
|
92
|
+
from `--engineer` and `--why`); the why is the session's first note. Then:
|
|
93
|
+
|
|
94
|
+
- **Select a runbook** and fill in its inputs: that starts its *run*. Run
|
|
95
|
+
buttons are disabled until then.
|
|
96
|
+
- **Work through the steps**, or open any step, in any order, and run just
|
|
97
|
+
that. A run is the record executions are written to, not an order you
|
|
98
|
+
have to follow (see [docs/concepts/runs.md](docs/concepts/runs.md#a-run-is-a-record-not-a-sequence)).
|
|
99
|
+
- **Switch runbooks** whenever you like. Every run stays open, and
|
|
100
|
+
selecting a runbook again returns to its run.
|
|
101
|
+
- **Write notes** on the session page as things happen.
|
|
102
|
+
- **End the session** with the button on the session page, or Ctrl-C.
|
|
103
|
+
Every run closes with the status its work earns: `completed`,
|
|
104
|
+
`partial`, or `opened`.
|
|
105
|
+
|
|
106
|
+
Everything you do, and every line of output, goes to a `session.log`
|
|
107
|
+
written as it happens and echoed to the terminal, like a Rails log:
|
|
108
|
+
|
|
109
|
+
```text
|
|
110
|
+
2026-10-10 01:17:19.589 INFO [disk-space-triage 010-check-free-space #acd50b22502a] execute bash via bash
|
|
111
|
+
2026-10-10 01:17:19.589 INFO [#acd50b22502a] $ df -h "$TARGET_DIR"
|
|
112
|
+
2026-10-10 01:17:19.602 INFO [#acd50b22502a] > /dev/disk3s5 1.8Ti 1.4Ti 391Gi 79% /System/Volumes/Data
|
|
113
|
+
2026-10-10 01:17:19.651 INFO [#acd50b22502a] finished exit 0 in 0.06s
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`--log-level`, `--verbose` and `--quiet` tune it; see
|
|
117
|
+
[docs/running/run-record.md](docs/running/run-record.md).
|
|
118
|
+
|
|
119
|
+
## A runbook is a directory
|
|
120
|
+
|
|
121
|
+
```text
|
|
122
|
+
staging-teardown/
|
|
123
|
+
runbook.md front matter + preamble (required)
|
|
124
|
+
steps/
|
|
125
|
+
010-confirm-nothing-to-preserve.md
|
|
126
|
+
020-stop-the-pipeline.md
|
|
127
|
+
030-drain-ecs-services.md
|
|
128
|
+
verify.md whole-procedure checks (optional)
|
|
129
|
+
rollback.md what to do when it fails partway (optional)
|
|
130
|
+
assets/ images referenced by the markdown
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The directory name is the runbook's slug. Steps are ordered by filename;
|
|
134
|
+
numeric prefixes with gaps leave room to insert.
|
|
135
|
+
|
|
136
|
+
### runbook.md
|
|
137
|
+
|
|
138
|
+
```yaml
|
|
139
|
+
---
|
|
140
|
+
title: Staging Infrastructure Teardown and Rebuild
|
|
141
|
+
when_to_use: >
|
|
142
|
+
The deployed stacks cannot be updated in place.
|
|
143
|
+
prerequisites:
|
|
144
|
+
- AWSAdministratorAccess in the staging account
|
|
145
|
+
- The staging branch is ready to fast-forward
|
|
146
|
+
blast_radius: >
|
|
147
|
+
Destroys the staging database, container images, and DNS.
|
|
148
|
+
escalation: Stop and contact the platform owner if any stack delete fails twice.
|
|
149
|
+
tags: [aws, staging, destructive]
|
|
150
|
+
inputs:
|
|
151
|
+
- name: AWS_PROFILE
|
|
152
|
+
prompt: AWS profile with admin access
|
|
153
|
+
default: staging-admin
|
|
154
|
+
- name: DB_PASSWORD
|
|
155
|
+
prompt: Database password
|
|
156
|
+
secret: true
|
|
157
|
+
interpreters:
|
|
158
|
+
ruby: bin/rails runner -
|
|
159
|
+
---
|
|
160
|
+
Everything below the front matter is the preamble and renders on the
|
|
161
|
+
landing page.
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
`inputs` become a form when a run starts. Values are exported as
|
|
165
|
+
environment variables to every executed block, so blocks refer to them as
|
|
166
|
+
`$NAME` and stay copy-pasteable into a terminal. A blank field falls back
|
|
167
|
+
to the value given for the same name earlier in the session (never for a
|
|
168
|
+
secret), then the process environment, then `default`. A `secret` input is masked in
|
|
169
|
+
the form and never written to the run record.
|
|
170
|
+
|
|
171
|
+
`interpreters` maps a language to the command that runs a file of it. The
|
|
172
|
+
defaults are `bash`, `sh`, `zsh` and `ruby`; a Rails project would map
|
|
173
|
+
`ruby` to `bin/rails runner -`, and `sql: psql -X -v ON_ERROR_STOP=1 -f`
|
|
174
|
+
makes `sql run` blocks execute through `psql`.
|
|
175
|
+
|
|
176
|
+
### Single-file runbooks
|
|
177
|
+
|
|
178
|
+
One markdown file works too: the front matter is the runbook's, every
|
|
179
|
+
`##` heading is a step, attributes go in an HTML comment after the
|
|
180
|
+
heading (`<!-- kind: verify, timeout: 30 -->`), and sections headed
|
|
181
|
+
**Verify** and **Rollback** stand in for the extra files. See
|
|
182
|
+
`examples/single_md_file_runbooks/db-maintenance.md`.
|
|
183
|
+
|
|
184
|
+
### Step files
|
|
185
|
+
|
|
186
|
+
```yaml
|
|
187
|
+
---
|
|
188
|
+
title: Drain the ECS services
|
|
189
|
+
kind: automated # automated | manual | verify
|
|
190
|
+
destructive: false
|
|
191
|
+
timeout: 600 # seconds, for executable blocks in this step
|
|
192
|
+
cwd: . # working directory, relative to the runbook
|
|
193
|
+
---
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
The body is ordinary markdown: the instruction, the command, and the "how to
|
|
197
|
+
tell it worked" line that every good step carries. `kind` defaults to
|
|
198
|
+
`automated` when the body has an executable block, otherwise `manual`.
|
|
199
|
+
|
|
200
|
+
## The fenced block convention
|
|
201
|
+
|
|
202
|
+
The info string's first word is the language. Any following words are
|
|
203
|
+
flags. A block with no flag is display only, whatever its language.
|
|
204
|
+
|
|
205
|
+
| Info string | Behaviour |
|
|
206
|
+
| --- | --- |
|
|
207
|
+
| ```` ```bash ```` | Display only. |
|
|
208
|
+
| ```` ```bash run ```` | Run button. Executed with the run's inputs in the environment; stdout, stderr and exit status recorded. |
|
|
209
|
+
| ```` ```bash destructive ```` | Run button behind a typed confirmation code issued by the server. Implies `run`. |
|
|
210
|
+
| ```` ```ruby run ```` | Same, executed with `ruby` (or the runbook's `interpreters.ruby`). |
|
|
211
|
+
| ```` ```bash background ```` | Start and Stop buttons. No timeout; output streams into the page; listed in the sidebar's Running panel; stopped when the run ends. |
|
|
212
|
+
| ```` ```bash terminal ```` | No Run button. "Run this in your own terminal, then confirm." An **I ran this** button records the confirmation, with an optional note. |
|
|
213
|
+
| ```` ```text expect ```` | Not executed. Compared with the output of the executable block above it; a mismatch is shown side by side with the differences marked. `{{any}}` matches anything within a line. |
|
|
214
|
+
|
|
215
|
+
An unknown flag, or `run` on a language that cannot execute, renders with a
|
|
216
|
+
visible warning instead of being ignored. `runsheets --check` lists them.
|
|
217
|
+
|
|
218
|
+
Each execution is a fresh process with the runbook directory as its working
|
|
219
|
+
directory. Nothing carries over between blocks except the environment.
|
|
220
|
+
|
|
221
|
+
## The run record
|
|
222
|
+
|
|
223
|
+
```text
|
|
224
|
+
~/.local/share/runsheets/runs/
|
|
225
|
+
sessions/<session-id>/
|
|
226
|
+
session.json who, when, the notes, the runs, how it ended
|
|
227
|
+
session.log every action and its output, appended as it happens
|
|
228
|
+
<runbook-slug>/<session-id>/
|
|
229
|
+
run.json steps, blocks, timestamps, exit codes, acknowledgements
|
|
230
|
+
run.md the same as a readable transcript
|
|
231
|
+
blocks/
|
|
232
|
+
020-stop-the-pipeline-1.1.cmd the exact code that ran
|
|
233
|
+
020-stop-the-pipeline-1.1.out its stdout + stderr
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Records live outside the runbook so captured output never lands next to
|
|
237
|
+
the docs in a repository. A runbook's landing page lists its previous
|
|
238
|
+
runs; each points back to its session. A session left running by a killed
|
|
239
|
+
process is closed as `interrupted` the next time runsheets starts.
|
|
240
|
+
|
|
241
|
+
## Security posture
|
|
242
|
+
|
|
243
|
+
- Binds to loopback by default. There is no multi-user story. `--bind` to
|
|
244
|
+
another address is possible, warns, and is a bad idea off a network you
|
|
245
|
+
control.
|
|
246
|
+
- Executes only blocks whose info string opts in. Everything else is inert.
|
|
247
|
+
- Every request needs this start's session key, which the browser gets by
|
|
248
|
+
opening the link runsheets prints. Another account on the same machine
|
|
249
|
+
can reach the port but sees nothing.
|
|
250
|
+
- Every state-changing request needs a per-process token that only a page
|
|
251
|
+
served by this process knows, and the `Host` header must be a loopback
|
|
252
|
+
name. Together these stop a page on another origin from driving the tool.
|
|
253
|
+
- Every page sets a Content-Security-Policy that lets only the page's own
|
|
254
|
+
script run, and the script acts only on blocks runsheets itself rendered.
|
|
255
|
+
Raw HTML in a runbook renders, but neither its script nor a copied Run
|
|
256
|
+
button does anything, so a runbook cannot drive the tool either.
|
|
257
|
+
- Anything a block leaves running (`cmd &`) is stopped when the block
|
|
258
|
+
finishes; a long-running process belongs in a `background` block.
|
|
259
|
+
- Destructive blocks show the blast radius and need a confirmation code the
|
|
260
|
+
server issues per block, checked server-side.
|
|
261
|
+
- Secret inputs reach the child process but never the run record. Captured
|
|
262
|
+
output is redacted before it is written; plain string replacement, so an
|
|
263
|
+
encoded secret is not caught.
|
|
264
|
+
- The tool writes nothing inside the runbook directory.
|
|
265
|
+
- Stopping the server ends the session: every run is closed and whatever it
|
|
266
|
+
left running is stopped. Secret values never reach the session log.
|
|
267
|
+
|
|
268
|
+
## Design
|
|
269
|
+
|
|
270
|
+
The model (`Runbook`, `Library`, `Step`, `Block`, `Renderer`, `Executor`,
|
|
271
|
+
`Expectation`, `Search`, `RunRecord`, `Run`, `Session`, `SessionLog`) has
|
|
272
|
+
no dependency on Sinatra and is tested in isolation; `Web` and `Pages` are
|
|
273
|
+
tested with rack-test.
|
|
274
|
+
|
|
275
|
+
## License
|
|
276
|
+
|
|
277
|
+
MIT. See [LICENSE.txt](LICENSE.txt).
|
data/bin/runsheets
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Make the script runnable straight from a checkout (bin/ on PATH, no
|
|
5
|
+
# `bundle exec`): put the sibling lib/ first on the load path. For an
|
|
6
|
+
# installed gem this resolves to the gem's own lib/, which is already there.
|
|
7
|
+
lib = File.expand_path("../lib", __dir__)
|
|
8
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
9
|
+
|
|
10
|
+
require "runsheets"
|
|
11
|
+
|
|
12
|
+
exit Runsheets::CLI.run(ARGV)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# About these examples
|
|
2
|
+
|
|
3
|
+
This file has no front matter, so it is a plain document, not a runbook.
|
|
4
|
+
It is not listed in the library tree; runbooks link to it.
|
|
5
|
+
|
|
6
|
+
Start runsheets on this directory to choose among the examples:
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
runsheets --open examples
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The examples are grouped by shape, and each group is a folder in the
|
|
13
|
+
library tree:
|
|
14
|
+
|
|
15
|
+
- `directory_based_runbooks/`: runbook directories, a `runbook.md` beside
|
|
16
|
+
a `steps/` folder holding one file per step.
|
|
17
|
+
- `single_md_file_runbooks/`: single-file runbooks, every step in one
|
|
18
|
+
markdown file, together with the plain documents they link to.
|
|
19
|
+
`runbooks_with_scripts/` inside it holds a runbook whose blocks call
|
|
20
|
+
asgard tasks from the `.loki` files beside it.
|
|
21
|
+
|
|
22
|
+
| Example | Shape | Safe to run |
|
|
23
|
+
| --- | --- | --- |
|
|
24
|
+
| [Hello, runsheets](directory_based_runbooks/hello/runbook.md) | runbook directory | yes |
|
|
25
|
+
| [Disk space triage](single_md_file_runbooks/disk-space-triage.md) | single file | yes, it only reads |
|
|
26
|
+
| [Monthly PostgreSQL maintenance](single_md_file_runbooks/db-maintenance.md) | single file | needs a database |
|
|
27
|
+
| [Staging infrastructure teardown](directory_based_runbooks/staging-teardown/runbook.md) | runbook directory | no, read it |
|
|
28
|
+
| [Runbook tasks from local .loki files](single_md_file_runbooks/runbooks_with_scripts/asgard-tasks.md) | single file with `.loki` task files | yes, it only reads; needs asgard |
|
|
29
|
+
|
|
30
|
+
## Plain documents
|
|
31
|
+
|
|
32
|
+
Besides this file, two plain documents back the disk space triage runbook:
|
|
33
|
+
[the disk usage glossary](single_md_file_runbooks/disk-usage-glossary.md) beside it, and
|
|
34
|
+
[the cleanup policy](single_md_file_runbooks/policies/cleanup-policy.md) in a folder of its own.
|
|
35
|
+
|
|
36
|
+
A runbook can link to a plain document anywhere under the directory
|
|
37
|
+
runsheets was started on (here, `examples/`), with an ordinary relative
|
|
38
|
+
link. Started on a single runbook instead (`runsheets examples/directory_based_runbooks/hello`),
|
|
39
|
+
that runbook's own directory is the limit, so a link climbing out of it,
|
|
40
|
+
like the one `directory_based_runbooks/hello/runbook.md` makes to this file, does not resolve.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Rollback
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Nothing in this runbook needs rolling back. In a real runbook this file holds
|
|
6
|
+
the steps that undo a partial run, and it sits one click away in the sidebar
|
|
7
|
+
of every step so nobody has to scroll for it when something goes wrong.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
echo "nothing to undo"
|
|
11
|
+
```
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Hello, runsheets
|
|
3
|
+
when_to_use: >
|
|
4
|
+
You want to see what an executable runbook looks like, or you need a
|
|
5
|
+
fixture that exercises every block kind without touching anything real.
|
|
6
|
+
prerequisites:
|
|
7
|
+
- A shell with bash and ruby on the PATH
|
|
8
|
+
- Nothing else; every command here is harmless
|
|
9
|
+
escalation: There is nobody to call. Read the step again.
|
|
10
|
+
tags: [example, safe]
|
|
11
|
+
inputs:
|
|
12
|
+
- name: NAME
|
|
13
|
+
prompt: Who should be greeted?
|
|
14
|
+
default: world
|
|
15
|
+
- name: SECRET_WORD
|
|
16
|
+
prompt: A word that must never appear in the run record
|
|
17
|
+
default: hunter2
|
|
18
|
+
secret: true
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
# Hello, runsheets
|
|
22
|
+
|
|
23
|
+
This runbook exists to show the shape of a runbook directory and the fenced
|
|
24
|
+
block convention. Nothing in it changes your machine.
|
|
25
|
+
|
|
26
|
+
A runbook is a directory:
|
|
27
|
+
|
|
28
|
+
```text
|
|
29
|
+
hello/
|
|
30
|
+
runbook.md this file: front matter and preamble
|
|
31
|
+
steps/*.md one file per step, ordered by filename
|
|
32
|
+
verify.md whole-procedure checks
|
|
33
|
+
rollback.md what to do when it goes wrong
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Start a run from the panel above, then walk the steps in the sidebar.
|
|
37
|
+
|
|
38
|
+
This runbook sits in a directory of examples. [About these
|
|
39
|
+
examples](../../about-the-examples.md) is a plain document two levels up,
|
|
40
|
+
linked like any other markdown file.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Say hello
|
|
3
|
+
kind: automated
|
|
4
|
+
timeout: 30
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
The inputs you gave when starting the run are exported as environment
|
|
8
|
+
variables, so a block can refer to them as `$NAME` and still be pasted into a
|
|
9
|
+
terminal unchanged.
|
|
10
|
+
|
|
11
|
+
```bash run
|
|
12
|
+
echo "Hello, $NAME!"
|
|
13
|
+
echo "Running block $RUNSHEETS_BLOCK of step $RUNSHEETS_STEP in run $RUNSHEETS_RUN_ID"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
A `text expect` block after it shows what the output should look like.
|
|
17
|
+
Once the block has run, the page compares the two and hides the expect
|
|
18
|
+
block: a match just says so, and a mismatch shows both side by side with
|
|
19
|
+
the differences marked. The run id is different every time, so the
|
|
20
|
+
expected text has `{{any}}` in its place, which matches anything on that
|
|
21
|
+
line. It is a hint, not a verdict.
|
|
22
|
+
|
|
23
|
+
```text expect
|
|
24
|
+
Hello, world!
|
|
25
|
+
Running block 010-say-hello-1 of step 010-say-hello in run {{any}}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
A block with no flag is just displayed. This one is the same command, but it
|
|
29
|
+
has no Run button:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
echo "Hello, $NAME!"
|
|
33
|
+
```
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Inspect the Ruby that will run things
|
|
3
|
+
kind: automated
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Ruby blocks execute with `ruby` unless the runbook's front matter maps the
|
|
7
|
+
language to another command, such as `bin/rails runner -`.
|
|
8
|
+
|
|
9
|
+
```ruby run
|
|
10
|
+
puts "Ruby #{RUBY_VERSION} on #{RUBY_PLATFORM}"
|
|
11
|
+
puts "cwd: #{Dir.pwd}"
|
|
12
|
+
puts "greeting for: #{ENV.fetch('NAME', '(unset)')}"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Other languages stay display only until there is a safe way to route them.
|
|
16
|
+
This SQL is for a client you open yourself:
|
|
17
|
+
|
|
18
|
+
```sql
|
|
19
|
+
SELECT count(*) FROM runs WHERE status = 'completed';
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Some things cannot be run from a page at all, because they are interactive.
|
|
23
|
+
Do them in your own terminal, then click **I ran this** so the run record
|
|
24
|
+
shows you did, with a note if you want one:
|
|
25
|
+
|
|
26
|
+
```bash terminal
|
|
27
|
+
read -r -p "Press Enter when you have looked at the output above: "
|
|
28
|
+
```
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Take a breath
|
|
3
|
+
kind: manual
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Not every step is a command. A manual step is an instruction; the operator
|
|
7
|
+
reads it, does it, and marks the step done, optionally with a note that goes
|
|
8
|
+
into the run record.
|
|
9
|
+
|
|
10
|
+
1. Look at the sidebar. Steps that have run show a mark next to them.
|
|
11
|
+
2. Decide whether you want to see a failure next, or skip it.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Keep a clock running
|
|
3
|
+
kind: automated
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Some things have to stay up while you work through later steps: a tunnel, a
|
|
7
|
+
log tail, a port forward. A `background` block has Start and Stop buttons
|
|
8
|
+
instead of Run. Its output streams into the page while it runs, it has no
|
|
9
|
+
timeout, and it is listed in the sidebar's **Running** panel on every page
|
|
10
|
+
with a Stop button of its own.
|
|
11
|
+
|
|
12
|
+
```bash background
|
|
13
|
+
while true; do
|
|
14
|
+
date "+%H:%M:%S still here"
|
|
15
|
+
sleep 1
|
|
16
|
+
done
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Leave it running and move to the next step, or stop it here. Anything still
|
|
20
|
+
running when the run is finished is stopped for you, and the execution is
|
|
21
|
+
recorded as `stopped` rather than as a failure.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Exercise a failure
|
|
3
|
+
kind: automated
|
|
4
|
+
destructive: true
|
|
5
|
+
timeout: 5
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
This step is marked destructive in its front matter even though it does no
|
|
9
|
+
harm, so you can see the confirmation flow. Both blocks are safe.
|
|
10
|
+
|
|
11
|
+
The first one exits non-zero on purpose. The page shows the exit status and
|
|
12
|
+
the run record keeps it:
|
|
13
|
+
|
|
14
|
+
```bash destructive
|
|
15
|
+
echo "about to fail on purpose" >&2
|
|
16
|
+
exit 3
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The second one sleeps longer than the step's five second timeout, so the
|
|
20
|
+
process group is killed and the execution is recorded as timed out:
|
|
21
|
+
|
|
22
|
+
```bash run
|
|
23
|
+
echo "sleeping"
|
|
24
|
+
sleep 30
|
|
25
|
+
echo "never printed"
|
|
26
|
+
```
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Check the greeting
|
|
3
|
+
kind: verify
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
A `verify` step is a read-only check. It runs in its place during the
|
|
7
|
+
procedure, and it also appears on the **Checks** page with every other
|
|
8
|
+
check, where **Run all** runs them one after another inside the run.
|
|
9
|
+
|
|
10
|
+
```bash run
|
|
11
|
+
echo "Hello, $NAME!" | grep -q "Hello, $NAME!" && echo "greeting looks right"
|
|
12
|
+
ruby -e 'puts "ruby answers: #{6 * 7}"'
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```text expect
|
|
16
|
+
greeting looks right
|
|
17
|
+
ruby answers: 42
|
|
18
|
+
```
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Verify
|
|
3
|
+
kind: verify
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Whole-procedure checks. These should pass after every run.
|
|
7
|
+
|
|
8
|
+
```bash run
|
|
9
|
+
test -n "$NAME" && echo "NAME is set to $NAME"
|
|
10
|
+
test -d "$RUNSHEETS_RUN_DIR/blocks" && echo "run directory exists: $RUNSHEETS_RUN_DIR"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
A secret input never reaches the run record, even when a block prints it.
|
|
14
|
+
The value is replaced before the output is written:
|
|
15
|
+
|
|
16
|
+
```bash run
|
|
17
|
+
echo "the secret is $SECRET_WORD"
|
|
18
|
+
```
|