Workflow management with less hassle.
The medic workflow is intended to quickly bootstrap development on a project. Steps and checks verify that all dependencies are satisfied, with suggested remedies. Quickly applying those remedies can get your workstation up and running... or you may meet the dependency requirement yourself.
Checks should be written (where possible) such that they only care about the dependency being met, rather than how—remedies might be applied as-is on a shared workstation, while an individual might choose alternate solutions on their personal hardware. New versions of operating systems and/or dependency management tools may make usual remedies temporarily unworkable, requiring manual workarounds for a day or a week until further upstream updates fix the problem... that's ok! Over time you can make your checks more resilient to alternatives.
Medic should above all else assist its adopters, and only where necessary or on some teams should it proscribe a specific remedy.
doctor
- is everything set up on my project?test
- run all tests, for all test suites.audit
- do all my lints and security checks pass?outdated
- how do I update dependencies across multiple toolchains?update
- pull changes and apply steps such as db migrations or deps updates.shipit
- run everything before pushing upstream.
brew install synchronal/tap/medic
# optionally add extensions which include steps, checks, and/or outdated checks:
brew install synchronal/tap/medic-ext-elixir
brew install synchronal/tap/medic-ext-git
brew install synchronal/tap/medic-ext-homebrew
brew install synchronal/tap/medic-ext-node
brew install synchronal/tap/medic-ext-rust
brew install synchronal/tap/medic-ext-tool-versions
brew install synchronal/tap/medic-ext-postgres
Medic provides a set of subcommands, each of which reads configuration
from a TOML-formatted file with a default path of
.config/medic.toml
. This can be overridden with -c
, --config
, or
by setting the MEDIC_CONFIG
environment variable.
medic init # -- add an empty medic config manifest to a project.
medic doctor # -- ensure a project is fully set up for development.
medic test # -- run all test commands.
medic audit # -- run lints, type checks, dependency audits, etc.
medic outdated # -- check for outdated project dependencies.
medic update # -- update the project with upstream changes.
medic shipit # -- run all checks and ship your changes.
medic run # -- runs a shell command with medic progress output.
Subcommands (with the exception of init
and run
) may be run
interactively via -i
, --interactive
, or by assigning
MEDIC_INTERACTIVE=true
in the shell. When run interactively, if checks
fail with suggested remedies, medic will prompt the user for action.
Remedies may be automatically applied, skipped, or medic may be entirely
quit.
medic init
creates a medic config manifest in the current directory,
defaulting to ./.config/medic.toml
.
medic doctor
runs checks to ensure the project is ready for a
developer to work on a project.
Examples:
- Are all dependencies installed?
- Are all required databases installed and accessible?
- Are development services and fakes running?
- Are test services available?
Valid actions:
medic test
runs all tests. Useful not just for documenting the test
suite used for your project, but for when multiple test suites are used
(ExUnut + bats
, etc)
Valid actions:
- checks
- doctor - specified with
{doctor = {}}
. - shell actions
- steps
medic audit
is intended for anything that has to do with formatting or
security.
Examples:
- is the code properly formatted (
mix format --check-formatting
,prettier --check
, etc)? - linters (
cargo clippy
, etc) - dependency audits (
mix_audit
,cargo-audit
,npm audit
, etc)
Valid actions:
medic outdated
checks for dependencies that might be updatable.
Examples:
- language
- runtime version manager (asdf, rtx)
- packages (cargo, mix, pip)
See Outdated checks for examples of writing new checks.
medic update
updates the project with upstream changes.
Examples:
- git pull
- install dependencies (automatically, without checking)
- compile dependencies
- run database migrations
- run
medic doctor
Valid actions:
- checks
- doctor - specified with
{doctor = {}}
. - shell actions
- steps
A typical update
configuration pulls changes from remote source
control, runs any automated steps that should be applied on every pull
(install 3rd-party libraries or run database migrations, for instance),
then runs doctor
to verify that any new checks that have been pulled
are run.
It's a fairly regular occurrence to find or create race conditions
between update
and doctor
, for example:
- A new step added to
update
may only succeed if new checks indoctor
have been run. update
may automatically apply steps that allow a project work, but where nodoctor
check has been added. One's current workstation, wheremedic update
is run more often thanmedic doctor
, may continue to work, while a new person to the project may find that doctor does not leave them in a position to start work.
Upon a failure of medic update
one may want to have the habit of
immediately running medic doctor
. When making changes to update
configuration, one may want to try to manually undo the changes and
see if medic doctor
results in a working installation.
medic shipit
runs all necessary actions to ship new code in a safe
manner.
Valid actions:
- checks
- shell actions
- steps
- audit - specified with
{audit = {}}
. - test - specified with
{test = {}}
. - update - specified with
{update = {}}
.
A typical shipit
configuration runs audit
, then update
, then
test
, then whatever additional checks and steps are required to build
a project for release, then runs a step or shell action to push changes
to remote source control.
medic run
executes an arbitrary shell command.
Arguments:
--name <name>
- used in progress indicators.--cmd <cmd>
- the shell command to execute.--remedy <remedy>
- an optional remedy to output if the command fails.--verbose
- optionally writes output to the terminal alongside running progress.
Each command runs a set of checks and/or steps, with some commands optionally accepting configuration indicating that medic should run another of its commands.
check
: Run the shell commandmedic-check-{name}
with an optional subcommand and optional args. If the check provides a remedy (see below), then upon failure the remedy will be added to the system clipboard.shell
: Will run the specified shell command as-is.verbose
- print all stdout/stderr to the terminal as it happens.allow_failure
- continue medic even if the command fails.
step
: Runs the shell commandmedic-step-{name}
with optional subcommand and args.verbose
- print all stdout/stderr to the terminal as it happens.allow_failure
- continue medic even if the command fails.
[doctor]
checks = [
# the following executes: `medic-check-tool-versions plugin-installed --plugin rust`
{ check = "tool-versions", command = "plugin-installed", args = { plugin = "rust" } },
# the following executes: `medic-check-tool-versions package-installed --plugin rust`
{ check = "tool-versions", command = "package-installed", args = { plugin = "rust" } },
# the following executes: `medic-check-homebrew`
{ check = "homebrew", verbose = true, output= "stdio" },
# the following executes: `medic-check-rust crate-installed --name cargo-audit --name cargo-outdated`
{ check = "rust", command = "crate-installed", args = { name = ["cargo-audit", "cargo-outdated"] } },
# ... etc
{ check = "rust", command = "target-installed", args = { target = "aarch64-apple-darwin" } },
{ check = "rust", command = "target-installed", args = { target = "x86_64-apple-darwin" } },
]
[test]
checks = [
{ name = "Check for warnings", shell = "cargo build --workspace --features strict" },
# the following executes: `medic-step-rust test`
{ step = "rust", command = "test", verbose = true },
]
[audit]
checks = [
{ name = "Audit crates", shell = "cargo audit", allow_failure = true, verbose = true },
{ check = "rust", command = "format-check" },
{ name = "Shell format check", shell = "cargo fmt --check", remedy = "cargo fmt" },
{ step = "rust", command = "clippy" },
]
[outdated]
checks = [
# the following executes: `medic-outdated-rust`
{ check = "rust" },
# the following executes: `(cd crates/sub-crate && medic-outdated-rust)`
{ check = "rust", cd: "crates/sub-crate" },
]
[update]
steps = [
{ step = "git", command = "pull" },
{ doctor = {} },
]
[shipit]
steps = [
{ audit = {} },
{ update = {} },
{ test = {} },
{ name = "Release", shell = "bin/dev/release", verbose = true },
{ step = "git", command = "push" },
{ step = "github", command = "link-to-actions", verbose = true },
]
Custom checks may be run, so long as they are named medic-check-{name}
and are available in the PATH.
check
- REQUIRED - name of the check. Shells out tomedic-check-{name}
, which is expected to be available in the PATH.command
- an optional subcommand to pass as the first argument to the check.args
- a map of flag to value(s). When running the command, the flag name will be translated to--flag <value>
. When the value is specified as a list, the flag will be output once per value.cd
- change directory before running checks.env
- environment variables to set when running checks.output
- the output format used by the check, eitherjson
orstdio
verbose
- whentrue
, STDERR of the check is redirected to STDERR of the current medic process.
# runs: (cd ./subdir \
# && MEDIC_OUTPUT_FORMAT=json VAR=value \
# medic-check-my-check sub-option --with thing --and first --and second)
{
check = "my-check",
command = "sub-option",
args = { with = "thing", and = ["first", "second"]},
cd = "./subdir",
env = { VAR = "value" },
verbose = true
}
Checks must follow one or more output format, which is provided to the
check in the environment: variable MEDIC_OUTPUT_FORMAT
:
- Informational output may be written to STDERR in any format. If the
check is configured with
verbose = true
, this output will be written directly to the STDERR of medic as it happens. - JSON should be written to STDOUT with the following optional keys:
{ "output": "Output to display to the user, for example STDOUT captured from internal commands", "error": "Error to display to the user", "remedy": "suggested remedy to resolve the problem" }
- If the check fails, the process must exit with a non-zero exit status.
Note that upon failure, the error
key in the output JSON takes
priority over STDERR.
- Informational output may only be written to STDERR.
- The suggested remedy (if available) must be written to STDOUT.
- If the check fails, the process must exit with a non-zero exit status.
Custom steps may be run, so long as they are named medic-step-{name}
and are available in the PATH. Steps must follow:
- Informational output may be written to STDERR or STDOUT.
- If the step fails, the process must exit with a non-zero exit status.
command
- an optional subcommand to pass as the first argument to the check.args
- a map of flag to value(s). When running the command, the flag name will be translated to--flag <value>
. When the value is specified as a list, the flag will be output once per value.cd
- change directory before running checks.env
- environment variables to set when running steps.verbose
- print all stdout/stderr to the terminal as it happens.allow_failure
- continue medic even if the command fails.
# runs: (cd ./subdir \
# && VAR=value \
# medic-step-my-step sub-option --with thing --and first --and second)
{
step = "my-step",
command = "sub-option",
args = { with = "thing", and = ["first", "second"]},
cd = "./subdir",
env = { VAR = "value" },
verbose = true
}
Arbitrary shell actions can be run. If the shell command returns a non-zero exit status, then the action is deemed a failure.
Note that pipes and redirections are not handled, so complex shell commands may be better suited to be written into shell scripts.
name
- the description to be shown to the user when run.shell
- the command to runcd
- change directory before running commands.env
- environment variables to set when running commands.inline
- whentrue
, disables running progress bars and prints all output directly to the terminal. This flag takes priority oververbose
, and is useful when running commands that handle their own progress indicators, for example when usingmedic run
from shell scripts.verbose
- whentrue
, STDOUT and STDERR of the action are printed as to the console alongside running progress.allow_failure
- allow medic to continue even when the process fails.remedy
- an optional command to print out on failure to suggest as a remediation.
# runs: (cd ./subdir \
# && VAR=value \
# ls -al ./some/dir)
{
name = "my shell step",
shell = "ls -al ./some/dir",
remedy = "mkdir -p ./some/dir",
cd = "./subdir",
env = { VAR = "value" },
verbose = true
}
Outdated checks work differently from other types of checks.
These checks run commands named medic-outdated-{check}
that must be
found in the PATH. These commands must follow these rules:
- Informational output must be written to STDERR.
- An outdated dependency must be written to STDOUT in one of the
following formats:
::outdated::name=<name>::version=<version>::latest=<latest>
::outdated::name=<name>::version=<version>::latest=<latest>::parent=<parent>
- An optional remedy for updating dependencies may be output to STDOUT
in the following format:
::remedy::<command>
Values to be included:
name
- the name of the dependency.version
- the version of the dependency currently used.latest
- the most current available version of the dependency.parent
- if the project does not explicitly declare this dependency,parent
may be set to show why this is appearing in outdated content.
Output from steps and checks can be colorized using unicode or hexadecimal ANSI escape sequences.
echo "\u001b[1;31mHere is some red text\u001b[0m" >&2
echo "\x1b[1;33mHere is some yellow text\x1b[0m" >&2
brew bundle
bin/dev/doctor
cargo run --bin medic-doctor -- -c fixtures/medic.toml
- This project uses the unstable feature
try_trait_v2
, which requires nightly Rust. Until the feature is made stable, compilation of this project could break at any time with changes to nightly Rust.