# Howzit build notes — authoring reference This file tells an agent how to write a Howzit build notes file. Howzit is documentation-first: a Markdown file of project procedures that can also be run as tasks. Write notes a human can read, and mark the parts that should execute. Do not invent directives, metadata keys, or variable syntax that are not described here. ## File Put one file in the project root. The name must start with `build` or `howzit` and end in `.md`, `.txt`, or `.markdown`. - Shared notes: `buildnotes.md` - Notes that should stay out of git: `buildprivate.md` (add that name to the global gitignore) Anything before the first Markdown heading is ignored as content. Metadata (below) is the exception: key/value lines before the first heading are read as metadata. Use a level-1 heading for the project title when topics are level 2: ```markdown author: Ada Lovelace license: MIT # Project title One-paragraph summary of the project. ## Build How to build it. ``` ## Topics and subtopics A topic is a Markdown heading of level 2 or deeper. The heading text is the topic title. Split topics with `##`. A deeper heading after a topic is a subtopic of it. The next heading at the same level or shallower ends the parent. ```markdown ## Deploy @run(./scripts/preflight.sh) Preflight checks ### Build assets @run(gulp) Build ### Upload @run(gulp sync) Sync to server ## Package management yarn ``` Rules: - `howzit deploy` shows the parent, then every subtopic. `howzit upload` shows just that subtopic. - `howzit -r deploy` runs the parent's own tasks, then each subtopic's tasks, depth-first, in document order. - `howzit -r upload` runs only that subtopic. - `@include(Deploy)` runs the parent and all of its subtopics. - A failed task skips the remaining subtopics unless the run uses `--force`. ## Metadata Before the first heading, write MultiMarkdown metadata: `key: value`, one pair per line. ```markdown template: markdown dirs: . docs environment: development # Project title ``` - Keys are case-insensitive and stored lowercase. - Per-file values override the same key in `~/.config/howzit/howzit.yaml` under `:metadata:`. - Every metadata value is available in the note as `${key}` and as `[%key]`. - `[%key:default]` uses `default` when that key is missing. - Command-line arguments win over metadata when both would fill the same `${name}`. ### template: Templates live in `~/.config/howzit/templates/` as `.md` files named for how you reference them (`ruby.md` is included with `template: ruby`). List several with commas: `template: ruby, markdown`. A template is itself a build note. Its topics are imported into the project note. - Include a subset: `template: ruby[Build | Test]` or `template: ruby[build*]`. `*` is a wildcard. Pipe-separate names. - A topic in the project note with the same title replaces the template topic. Do not include the template-name prefix when overriding. - A template may itself start with `template:` to pull in other templates. - In the template, require metadata the project must set: ```markdown required: executable, dirs ``` Comma-separate multiple keys. Howzit errors if an included template's required key is missing from the project note. ### includes: Pull topics from another build note or from a project directory: ```markdown includes: ~/Code/other-project, ~/shared/tasks.md includes: ~/Code/other[Deploy | Test*] ``` - A directory is searched for a build note the same way as the current directory. - A file path is loaded directly. - Included topics are prefixed with the directory or file basename, for example `other-project:Deploy`. - A same-named topic in the local note wins, and the included one is skipped. - `includes:` inside an included file is not processed. Templates referenced by an included file still load. `includes:` copies topics in at load time. `@include(Topic)` runs another topic's tasks at run time. They are different. ### default: Name the topics to run for `howzit -r` with no topic argument: ```markdown default: Build, Test, Deploy default: Build, Deploy[production, fast] ``` - Each entry is matched like a topic on the command line. If several topics match, the first match is used. - Square brackets pass arguments to that topic, same syntax as `@include(Topic [arg])`. Commas inside the brackets separate that topic's arguments. - A missing entry is an error; the other entries still run. If none match, Howzit exits with an error. - The first failed task stops the rest unless `--force` is used. - `default:` does not change viewing. `howzit` with no arguments still shows every topic. - A topic titled "Default" cannot be run as `howzit -r default`. ## Directives A directive is a line that starts with `@`. `howzit -r TOPIC` executes every directive in that topic and its subtopics, in order, and stops at the first failure. Text after the closing `)` is the task title. Viewers see the title. `--show-code` shows the directive body instead. ```markdown @run(~/scripts/deploy.sh) Deploy script ``` ### @run ```markdown @run(./script.sh) Run script @run(./script.sh, log_level=debug) Run with debug logging @run(./build.sh, log_level=warn) Warnings and errors only ``` The command runs in the shell's current directory. `log_level` is `debug`, `info`, `warn`, or `error`, and applies only to that command. It filters `LOG:` messages from the script. ### @copy and @open ```markdown @copy(npm run build && rsync -av dist/ server:) Copy the release command @open(https://example.com/docs) Open the docs @open(README.md) Open the readme ``` `@copy` copies the text to the clipboard. `@open` opens a file or URL with the platform opener (`open` on macOS, `start` on Windows, `xdg-open` on Linux). Both always count as success. ### @include ```markdown @include(Test) @include(Test [rspec]) @include(Test [${default_test}]) ``` Imports and runs every task from the first topic whose title matches (partial match is allowed). Square brackets pass arguments to that topic, separated by commas. Those arguments do not change arguments passed on the command line to the topic that contains the `@include`. `@include` fails if any task inside the included topic fails, and that failure stops the caller. ### @set_var ```markdown @set_var(VERSION, "1.2.3") @set_var(VERSION, `rake vver`) @set_var(BUILD_DATE, $(date +%Y-%m-%d)) @set_var(NEXT_VERSION, `echo ${PREV_VERSION} | awk -F. '{print $1"."$2"."$3+1}'`) ``` Sets a variable for later conditions and for `${VAR}` in later directives. The name is one word of letters, digits, dashes, and underscores. The value is a quoted or unquoted string, or command output from backticks or `$()`. Output is trimmed. `${VAR}` inside a command is substituted before the command runs. Directives run in order, so a variable is available to later `@set_var` lines. ### @log_level ```markdown @log_level(debug) @run(./verbose-script.sh) All messages @log_level(warn) @run(./quiet-script.sh) Warnings and errors only ``` Sets the log level for every later `@run` and run block in the topic. Levels: `debug`, `info`, `warn`, `error`. Use it more than once to change level between tasks. ### @before and @after ```markdown @before - [ ] Tests pass - [ ] Changelog updated @end @run(./deploy.sh) Deploy @after - [ ] Tag the release - [ ] Announce in chat @end ``` `@before` ... `@end` is a prerequisite checklist. It is shown in a box before the run, then Howzit asks for a yes/no confirmation. `@after` ... `@end` is shown in a box after the run finishes. The tags themselves are hidden when the topic is viewed. The content between them is still shown. Directives inside either block are not delayed for the prompt; they run in document order with everything else. Inside the box they are displayed raw. When the topic has subtopics or `@include`s, every `@before` is gathered into one box and one confirmation, and every `@after` is gathered into one box at the end. Order: the topic's own blocks, then blocks from included topics in `@include` order, then blocks from subtopics. A topic included more than once contributes its blocks once. If a later subtopic is skipped because of a failure, its `@after` notes are still shown. Variables in the blocks are filled in, including arguments passed with `@include(Topic [arg])`. ### Optional directives Append `?` to ask before running. Default answer is yes (pressing return accepts). Append `?!` to make the default no. ```markdown @run?(./deploy.sh) Deploy @include?!(Dangerous reset) Reset ``` This works on any directive. A declined directive is skipped, not failed. For a fenced script, use `run?` or `run?!` as the language tag. ## Run blocks For a short script that does not deserve its own file, use a fenced block tagged `run`: ````markdown ```run Just Testing #!/bin/bash log info "Starting" echo "A few commands" set_var STATUS "ok" ``` ```` - The optional title after `run` is the task title. - The body is written to a temp file and executed. - The hashbang selects the interpreter (`#!/bin/bash`, `#!/usr/bin/env ruby`, `#!/usr/bin/env python3`). With no hashbang, the block runs under `sh`. - Howzit injects helper functions into run blocks so the script can call `log` and `set_var` (see Script communication). Helpers are not injected for `@run(external-script)`. - A block fails only if its final exit status is non-zero. A failing command in the middle of a Bash block does not fail the block unless the script uses `set -e`. - If a topic contains both `@` directives and `run` blocks, the `@` directives run first. ## Variables Pass arguments after `--` on the command line: ```text howzit -r test -- -x "arg 1" arg2 ``` Inside `@run` and non-shell run blocks: - `$1`, `$2`, ... are positional arguments. A placeholder with no argument is left as written (`$2` stays `$2`). - `$@` is every argument, shell-escaped, and is replaced only when arguments were passed. - `${1:default value}` is the fallback when that position was not passed. ### Named parameters Put names in parentheses on the title. They fill in order from the arguments after `--`. Names are letters, digits, and underscores. No spaces or hyphens. Separate names with commas. ```markdown ## Test (var1, var2) @run(cp "${var1}" "${var2}") ``` Refer to them as `${var1}` in Markdown and in commands. A default follows a colon: ```markdown ## Test (myvar:clobber package) @run(rake ${myvar}) ``` The same fallback can live on the placeholder instead: `${myvar:clobber package}`. Names are positional. If the first name has a default and you pass one argument, that argument fills the first name and overrides its default. To set the second name you must pass two arguments. Quote an argument that contains spaces. ### Shell run blocks In blocks whose hashbang is `sh`, `bash`, `zsh`, `dash`, or `ksh`, and in blocks with no hashbang, Howzit does not rewrite `${...}` in the script. It exports Howzit variables into the environment and lets the shell expand them. That includes topic parameters, metadata, `@set_var`, and variables set by earlier scripts. ````markdown ## Deploy (target:staging) ```run #!/bin/bash for file in dist/*; do echo "Uploading ${file} to ${target}" done echo "Using ${API_KEY:-no key} from the environment" ``` ```` - `${target}` is the Howzit variable. - `${file}` is the loop variable. If the script assigns a name that Howzit also set, the script's value wins. - Shell parameter expansion works, including `${VAR:-default}` and `${VAR:0:3}`. - Arguments after `--` are the script's own `$1`, `$#`, and `"$@"`. - Howzit defaults are rewritten to shell defaults: `${name:default}` becomes `${name:-default}`. A substring with a variable offset looks like a Howzit default, so write `${str:$start}` or `${str: start}`. - Single quotes suppress expansion: `'${target}'` is literal. Ruby, Python, Perl, Node, and fish blocks still have `${VAR}` replaced in the script text before they run. ### Escaping Where Howzit substitutes text (`@run`, `@copy`, Markdown, and non-shell blocks), write `$${VAR}` to leave `${VAR}` for the shell. Do this when a Howzit variable has the same name as a shell variable you want the shell to expand. Placeholders Howzit does not know are left alone, so `${HOME}` needs no escape when `HOME` is not a Howzit variable. ## Conditional blocks `@if` / `@unless` include or skip the lines up to the matching `@end`. The directive lines themselves never appear in output. Conditions are decided when the topic is processed, so a skipped branch is neither shown nor run. ```markdown @if CONDITION included when the condition is true @elsif OTHER included when CONDITION is false and OTHER is true @else included when none matched @end @unless CONDITION included when the condition is false @end ``` - Nest them. The first `@end` closes the most recent `@if` or `@unless`. Indentation does not matter. - Only the first matching branch is kept. - `@elsif` and `@else` work on both `@if` and `@unless`. - On a run, `@set_var`, `@log_level`, `@run`, `@include`, `@copy`, `@open`, and `run` blocks execute only on the branch that is taken. A skipped `@else` does not overwrite a variable set in the `@if`. ### Tests Negate with `not` or `!`. | Form | True when | | --- | --- | | `@if name` | `name` exists and is not empty | | `-z NAME` or `-z ${NAME}` | missing, unset, or only whitespace | | `-n NAME` | set and contains a non-whitespace character | | `==` | equal (string or number) | | `!=` | not equal | | `=~ /regex/` | regular expression match | | `*=` | contains the substring | | `^=` | starts with the prefix | | `$=` | ends with the suffix | | `**=` | fuzzy: the search characters appear in order | | `>` `>=` `<` `<=` | numeric comparison | ```markdown @if ${branch} =~ /^release-/ @if ${path} *= "src" @if ${file} ^= "test_" @if ${file} $= ".rb" @if ${my_var} **= "ffy" @if $BUILD_NUMBER >= 100 @if not -z branch @if ! -n VERSION ``` A bare word is looked up. A quoted string is a literal. Lookup order for a name: topic arguments (`## Deploy (option)` or `$1`), `${NAME}`, `@set_var` and script variables, metadata keys, environment variables. Metadata keys are case-insensitive. Topic variables and environment variables are case-sensitive. ### Special conditions ```markdown @if git dirty @if git clean @if file exists config.json @unless dir exists node_modules @if topic exists "Deploy" @if file contents VERSION.txt ^= 0. @if file contents CHANGELOG.md *= "Breaking" @if file contents config.json =~ /"version":\s*"1\./ @if cwd =~ /Code\/howzit/ @if working directory *= "project" ``` - `git dirty` / `git clean` describe uncommitted changes. - `file exists` and `dir exists` take a path. - `topic exists` takes a topic name. Useful in templates. - `file contents PATH OPERATOR VALUE` reads the file and compares it with any string operator above. The path may be a literal or a variable. - `cwd` and `working directory` are the current directory path. Used alone, `@if cwd` is always true. ## Script communication Scripts started by `@run` or a `run` block can log and set variables. Howzit sets `HOWZIT_COMM_FILE` to a temp file, reads it after the script exits, then deletes it. Append with `>>`. Do not hardcode the path. Helpers are injected only into `run` blocks. External scripts can source them from `$HOWZIT_SUPPORT_DIR` if they want the same functions. ```bash #!/bin/bash log info "Processing items..." log warn "Low disk space" log error "Failed to connect" log debug "Detail" set_var VERSION "1.2.3" ``` ```ruby #!/usr/bin/env ruby Howzit.logger.info("Processing items...") Howzit.set_var("VERSION", "1.2.3") ``` ```python #!/usr/bin/env python3 howzit.Howzit.logger.info("Processing items...") howzit.Howzit.set_var("VERSION", "1.2.3") ``` Fish has the same `log` and `set_var` functions as Bash. The manual format, which the helpers write for you: ```text LOG:info:Processing 42 items... LOG:warn:Low disk space detected LOG:error:Failed to connect LOG:debug:Detail VAR:BUILD_VERSION=1.2.3 ``` Log levels, low to high: `debug` (0), `info` (1), `warn` (2), `error` (3). A message is shown when its level is at least the current level. `HOWZIT_LOG_LEVEL` sets the default (`info` if unset). `@log_level` and `@run(..., log_level=...)` override it for the tasks they cover. `VAR:` names must match `[A-Z0-9_]+`. They join the same pool as other Howzit variables and override an earlier value of the same name. Timing: - Substitution in a task happens before that task runs. A script cannot read, in its own command line, a variable it sets. - The variable is available to later tasks in the topic: `${BUILD_VERSION}` in a later `@run`, a later title, Markdown text, and `@if` / `@unless`. - The next shell `run` block also receives it as an environment variable. `@run` commands and non-shell blocks do not; use `${VAR}` in their text. ```markdown ## Build @run(./get-version.sh) Get version @run(./build.sh --version=${BUILD_VERSION}) Build with version ``` ## Run behavior - `howzit -r build,test,deploy` runs those topics in order. - A non-zero `@run`, a non-zero `run` block, or a failed `@include` stops the topic, skips remaining subtopics, and skips later topics in the same run. - `--force` (`-f`) runs everything anyway. Failures are still reported. - After a run, Howzit prints a line per task: success, failure with exit code, or skipped because an earlier task failed. Tasks inside a conditional that was not evaluated are omitted. A skipped `@include` is listed as the included topic's individual tasks. ## Complete example ````markdown template: ruby includes: ~/shared/release-tasks.md default: Test, Build, Deploy[production] author: Project Author license: MIT # Widget Build, test, and ship the widget. ## Test (suite:rspec) @before - [ ] Dependencies installed @end @unless $SKIP_TESTS == "1" @run(bundle exec ${suite}) Run the suite @end ## Build (environment:development) @set_var(VERSION, `git describe --tags`) @if environment == "production" @run(./build.sh --release) Release build @elsif environment == "staging" @run(./build.sh --staging) Staging build @else ```run Development build #!/bin/bash log info "Building ${VERSION}" ./build.sh set_var STATUS "ok" ``` @end @if ${STATUS} == "ok" @run(echo "Built ${VERSION}") Report version @end ## Deploy (target) @if -z target @set_var(target, "staging") @end @unless git dirty @include(Test) @run(./deploy.sh ${target}) Deploy to ${target} @else Commit or stash changes before deploying. @end @after - [ ] Check ${target} @end ```` ## Checklist - File name starts with `build` or `howzit` and uses `.md`, `.txt`, or `.markdown`. - Metadata is `key: value` lines before the first heading. Topics start at `##` or deeper. - Every directive begins a line and uses a documented verb: `@run`, `@copy`, `@open`, `@include`, `@set_var`, `@log_level`, `@if`, `@unless`, `@elsif`, `@else`, `@before`, `@after`, `@end`. - Every `@if`, `@unless`, `@before`, and `@after` has its `@end`. - Task titles sit after the closing `)`. - Topic parameter names are `[A-Za-z0-9_]+`. Script `VAR:` names are `[A-Z0-9_]+`. - Shell `run` blocks use environment variables. Other languages use `${VAR}` substitution in the script text. - A variable set by a script or `@set_var` is used only in tasks that come after it.