Reports¶
Every command that inspects targets prints a report on stdout: text by default, JSON with
--format json, and for plan, distribute and doctor also Markdown with
--format markdown. Errors and CI annotations go to stderr, so JSON output stays valid.
| Report | Commands | Schema | Files in CI |
|---|---|---|---|
report/v1 |
plan, distribute |
touchmark schema report |
touchmark-report.json, touchmark-report.md; distribute also touchmark-report.jsonl |
doctor/v1 |
doctor |
touchmark schema doctor |
touchmark-doctor.json, touchmark-doctor.md |
setup/v1 |
setup |
touchmark schema setup |
— |
status/v1 |
status, apply |
touchmark schema status |
— |
check/v1 |
check |
touchmark schema check |
— |
What may change in these formats between releases, and what may not, is in Compatibility.
--report FILE (distribute, doctor) also writes the JSON report to a file;
--stream FILE (distribute) writes one JSON line per finished target as the run goes,
touchmark-report.jsonl by default in CI, which survives a killed job.
In a public hub, a target that is not public is never named: its entry has an empty path and repository id, and the text output only counts it.
plan and distribute¶
touchmark plan · hub acme-eng (github.com/712345678) @ 3f2a1c9 (PR #41) · touchmark 0.1.0
gh github.com read acme-read[bot] write acme-write[bot]: not checked resolve complete
corp gitlab.example.com read tm-reader write tm-writer: not checked resolve complete
scope: packs base, python changed → 11 of 11 targets (--all for every target)
opt_in: assumed: 2 targets without an opt-in file are opted in by targets.yml; an opt-in file with enabled: false opts one out
open 4 corp:platform/api, gh:acme/billing, gh:acme/docs and 1 more
update 1 gh:acme/sdk #1 (content)
unchanged 1 gh:acme/api #1
close 1 gh:acme/old #1 (no-diff)
skipped 3 gh:acme/archive (archived), gh:acme/web (not-opted-in); 1 private in public hub
blocked 1 gh:acme/cli #1 (branch-in-use)
Warnings
gh:acme/tools: orphaned 1: docs/python.md
Cost gh ≈ 16 writes (<1 min) · corp ≈ 2 writes (<1 min) · 1 run
In plan and distribute --dry-run an outcome means what distribute would do, and the
text says would. Every target has one outcome:
| Outcome | Reasons |
|---|---|
opened |
— |
updated |
content, rebase, recreate, body, title, base-renamed |
unchanged |
— |
closed |
no-diff, opted-out, target-dropped, duplicate |
declined |
— (the declined pull request is in pr) |
skipped |
not-opted-in, opted-out, archived, disabled, empty, mirror, pending-deletion, prs-disabled, sha256, unsafe-opt-in, private-in-public-hub, superseded |
blocked |
edited, branch-taken, branch-in-use, opt-in-invalid, marker-invalid, rules:<rule>, permission:<what>, cannot-sign, archived, mass-close |
deferred |
rate-limit, deadline, rollout-limit, provider-down, interrupted, cooldown |
failed |
transient, auth, access, git, integrity, race, secret-exposure, internal |
Troubleshoot says what each reason means. ops is the
audit journal of every write distribute made: when, by which account, to which target,
and the branch head before and after.
| Key | Type | Description |
|---|---|---|
schema |
report/v1, required |
Report format: report/v1. |
command |
one of plan, distribute, required |
The command that wrote the report. |
dry_run |
boolean | Set for distribute --dry-run: nothing was written, and outcomes and writes mean what distribute would do. |
engine |
string, required | touchmark's version. |
hub |
object, required | The hub the run read. |
hub.id |
string, required | hub.yml's id. |
hub.fingerprint |
string, required | The hub's host and immutable repository id, e.g. github.com/712345678; on Bitbucket the repository UUID, lowercase without braces; on Azure DevOps the repository id, lowercase. |
hub.commit |
string, required | Full commit id: 40 or 64 lowercase hex digits. |
hub.pr |
integer ≥ 1 | The hub pull request plan ran on. |
outcome |
one of completed, superseded, required |
completed, or superseded when the hub's default branch had moved on and nothing was inspected or written. |
strict |
boolean, required | The run had --strict: blocked and deferred targets and an incomplete resolve exit with code 3. |
scope |
object | The part of the fleet a plan in a hub pull request processed in full: every target (all), the targets whose final pack list holds a pack the pull request changes (packs), or none, when it changes only hub files no target receives (hub). The targets outside the scope are only counted. |
scope.mode |
one of all, packs, hub, required |
— |
scope.reason |
string | What decided the mode: --all, the files every target depends on that changed, or why the scope could not be computed. |
scope.packs |
list of strings | The packs the pull request changes. |
scope.base |
string | Full commit id: 40 or 64 lowercase hex digits. |
scope.processed |
integer ≥ 0, required | The targets the plan processed in full. |
scope.total |
integer ≥ 0, required | The targets the resolve found. |
assume_opt_in |
boolean | plan --assume-opt-in: for this report only, every target without an opt-in file, or whose opt-in file is not a regular file or is too large, counts as opted in; one whose opt-in file says enabled: false stays opted out. |
providers |
list of objects, required | The hub's providers, in hub.yml order. |
providers[].id |
string, required | — |
providers[].type |
one of github, gitlab, gitea, forgejo, bitbucket, azure-devops, bitbucket-datacenter, required |
— |
providers[].host |
string, not empty, required | The provider's host, with a port when it has one. |
providers[].reader |
string | Login of the read account. |
providers[].writer |
string | Login of the write account. |
providers[].write_check |
string, required | What is known about the write account's access, e.g. "not checked". |
providers[].resolve_complete |
boolean, required | Every organisation and group of targets.yml was listed in full. |
providers[].missing |
list of strings | Repositories targets.yml names that the platform does not know, as <provider>:<path>. |
providers[].error |
string | Why the provider was unavailable. |
targets |
list of objects, required | Every target, sorted by provider and path. |
targets[].provider |
string, required | — |
targets[].host |
string, not empty, required | — |
targets[].repo_id |
string, required | The repository's immutable id on the platform. |
targets[].path |
string, required | The repository's canonical path. |
targets[].outcome |
one of opened, updated, unchanged, closed, declined, skipped, blocked, deferred, failed, required |
— |
targets[].reason |
string, not empty | — |
targets[].pr |
object | — |
targets[].pr.number |
integer ≥ 1, required | — |
targets[].pr.url |
string | — |
targets[].pr.state |
one of open, closed, merged |
— |
targets[].key |
string | Content key of the changes; absent when there are none. |
targets[].packs |
list of strings | The packs the target gets, in layering order. |
targets[].opt_in_assumed |
boolean | The target was processed as if it had an empty opt-in file: it has none, or, with plan --assume-opt-in, one that is not a regular file or is too large. opt_in_assumed_by says why. |
targets[].opt_in_assumed_by |
one of targets.yml, --assume-opt-in |
Why opt_in_assumed is set: targets.yml (an entry with opt_in: assumed selects the target, for real) or --assume-opt-in (plan's flag, for the report only). |
targets[].changes |
object, required | The changes D holds, by action. |
targets[].changes.create |
integer ≥ 0, required | — |
targets[].changes.update |
integer ≥ 0, required | — |
targets[].changes.delete |
integer ≥ 0, required | — |
targets[].changes.chmod |
integer ≥ 0, required | — |
targets[].orphaned |
list of strings | Files a pack the target no longer gets shipped, still unchanged in the target. |
targets[].warnings |
list of strings | — |
targets[].writes |
integer ≥ 0, required | Writes for this target (HTTP writes of the API and git pushes): estimated by plan, counted by distribute. |
ops |
list of objects, required | Audit journal of every mutation distribute made; empty for plan. |
ops[].time |
string (date-time), required | — |
ops[].account |
string, required | Login of the account that wrote. |
ops[].target |
string, required | The target as <provider>:<path>. |
ops[].kind |
one of push, create-pr, edit-pr, comment, delete-branch, create-label, api-commit, update-refs, delete-ref, required |
What the mutation did: a git push, a pull request created or edited, a comment, a branch deleted, a label created, or a step of an API commit (the commit, the refs moved, a stage ref deleted). |
ops[].pr |
integer ≥ 1 | — |
ops[].before |
string | What the mutation replaced, e.g. the branch head before a push. |
ops[].after |
string | What the mutation left, e.g. the branch head after a push. |
warnings |
list of strings, required | Warnings about the run as a whole; warnings about one target are in the target. |
summary |
object, required | Targets per outcome, every outcome present. |
summary.opened |
integer ≥ 0, required | — |
summary.updated |
integer ≥ 0, required | — |
summary.unchanged |
integer ≥ 0, required | — |
summary.closed |
integer ≥ 0, required | — |
summary.declined |
integer ≥ 0, required | — |
summary.skipped |
integer ≥ 0, required | — |
summary.blocked |
integer ≥ 0, required | — |
summary.deferred |
integer ≥ 0, required | — |
summary.failed |
integer ≥ 0, required | — |
sweep |
object, required | The sweep of stale pull requests. |
sweep.ran |
boolean, required | — |
sweep.complete |
boolean, required | — |
sweep.failed |
boolean, required | — |
sweep.reason |
string | Why the sweep did not run. |
cost |
map of provider to integer ≥ 0, required | Writes per provider id (HTTP writes of the API and git pushes): estimated by plan, counted by distribute. |
estimate |
object | What the writes of a plan or a dry run take: per provider the writes and the time its write limits give them, and the runs the rollout needs under limits.max_new_prs_per_run. |
estimate.new_prs |
integer ≥ 0, required | Pull requests the rollout opens, in this run and the next ones. |
estimate.max_new_prs_per_run |
integer ≥ 0, required | — |
estimate.runs |
integer ≥ 0, required | Runs distribute needs to open them all: 0 when nothing is written, or when max_new_prs_per_run is 0 and some would open. |
estimate.providers |
map of provider to object, required | — |
estimate.providers.<provider>.writes |
integer ≥ 0, required | This run's writes by the git path: every push one write. |
estimate.providers.<provider>.seconds |
integer ≥ 0, required | — |
estimate.providers.<provider>.api_writes |
integer ≥ 0 | This run's writes when every push that needs or may need a signature goes through the platform's API commit; absent when that changes nothing. |
estimate.providers.<provider>.api_seconds |
integer ≥ 0 | — |
estimate.providers.<provider>.total_writes |
integer ≥ 0 | The writes of every run the rollout needs, by the git path; absent when it fits one run. |
estimate.providers.<provider>.total_seconds |
integer ≥ 0 | — |
paths |
list of objects | What the pushes of a plan or a dry run change, by path across the targets: sensitive paths first, then deletions, updates, mode changes and additions, each by path. |
paths[].action |
one of add, update, delete, chmod, required |
— |
paths[].path |
string, not empty, required | — |
paths[].sensitive |
boolean | The path matches a sensitive pattern (built-in or hub.yml's sensitive_paths) or is written executable. |
paths[].targets |
integer ≥ 1, required | How many targets the change goes to. |
operations |
list of objects | What each one-off operation does in a plan or a dry run: the entries of .touchmark/operations.yml in file order (recreate, forget_declines, allow_mass_close, adopt_unmarked), then the operation flags of a local run. |
operations[].kind |
one of recreate, forget_declines, allow_mass_close, adopt_unmarked, required |
— |
operations[].flag |
boolean | The operation is a flag of a local run, not an entry of operations.yml. |
operations[].target |
string, not empty | The entry's target as operations.yml writes it; absent for a target a public hub's report does not name. |
operations[].provider |
string | — |
operations[].pr |
integer ≥ 1 | The pull request the entry names (forget_declines) or whose branch it rebuilds (recreate). |
operations[].head |
string | Full commit id: 40 or 64 lowercase hex digits. |
operations[].max |
integer ≥ 1 | — |
operations[].until |
string | The last day the entry is in force: YYYY-MM-DD, UTC. |
operations[].effect |
one of applies, none, expired, unknown, required |
applies: the entry changes what the run does; none: it is in force and changes nothing; expired: its date has passed; unknown: the run cannot tell (its target was not inspected). |
operations[].detail |
string, not empty, required | — |
notes |
map of name to string | Free-form remarks about the run, by name. |
doctor¶
Each check has a name, a status (ok, warn, fail, unknown) and a detail, for the
hub, for each provider's write identity, and for each target. See
Check the write account.
| Key | Type | Description |
|---|---|---|
schema |
doctor/v1, required |
Report format: doctor/v1. |
command |
doctor, required |
— |
engine |
string, required | touchmark's version. |
hub |
object, required | The hub the run read. |
hub.id |
string, required | hub.yml's id. |
hub.fingerprint |
string, required | The hub's host and immutable repository id, e.g. github.com/712345678; on Bitbucket the repository UUID, lowercase without braces; on Azure DevOps the repository id, lowercase. |
hub.commit |
string, required | Full commit id: 40 or 64 lowercase hex digits. |
strict |
boolean, required | The run had --strict: a check that warns or is unknown, or an incomplete resolve, exits with code 3. |
hub_token |
boolean, required | doctor --hub-token read, with a maintainer's token, where the hub keeps its secrets. |
hub_checks |
list of objects, required | The checks of the hub itself. |
hub_checks[].check |
string, required | What was checked, e.g. access, token-expiry, rules, markers, hub-hidden, write-isolation, key-location. |
hub_checks[].status |
one of ok, warn, fail, unknown, required |
— |
hub_checks[].detail |
string | What the check found; absent for a target the report does not name. |
providers |
list of objects, required | The hub's providers, in hub.yml order. |
providers[].id |
string, required | — |
providers[].type |
one of github, gitlab, gitea, forgejo, bitbucket, azure-devops, bitbucket-datacenter, required |
— |
providers[].host |
string, not empty, required | The provider's host, with a port when it has one. |
providers[].writer |
string | Login of the writer hub.yml names. |
providers[].self |
string | Login the write credential acts as. |
providers[].resolve_complete |
boolean, required | Every organisation and group of targets.yml was listed in full. |
providers[].missing |
list of strings | Repositories targets.yml names that the platform does not know, as <provider>:<path>. |
providers[].error |
string | Why the provider could not be checked. |
providers[].checks |
list of objects, required | The checks of the write identity. |
providers[].checks[].check |
string, required | What was checked, e.g. access, token-expiry, rules, markers, hub-hidden, write-isolation, key-location. |
providers[].checks[].status |
one of ok, warn, fail, unknown, required |
— |
providers[].checks[].detail |
string | What the check found; absent for a target the report does not name. |
targets |
list of objects, required | Every target, sorted by provider and path; targets a public hub does not name come last within their provider. |
targets[].provider |
string, required | — |
targets[].host |
string, not empty, required | — |
targets[].repo_id |
string, required | The repository's immutable id on the platform; empty for a target the report does not name. |
targets[].path |
string, required | The repository's canonical path; empty for a target the report does not name. |
targets[].skipped |
string, not empty | Why the target was not checked: archived, disabled, empty, mirror, pending-deletion, prs-disabled, sha256, not-opted-in, opted-out, unsafe-opt-in, or deferred:<reason> (the run could not check the target; an unknown check says why). |
targets[].checks |
list of objects, required | — |
targets[].checks[].check |
string, required | What was checked, e.g. access, token-expiry, rules, markers, hub-hidden, write-isolation, key-location. |
targets[].checks[].status |
one of ok, warn, fail, unknown, required |
— |
targets[].checks[].detail |
string | What the check found; absent for a target the report does not name. |
warnings |
list of strings, required | Warnings about the run as a whole. |
summary |
object, required | Checks per status, every status present. |
summary.ok |
integer ≥ 0, required | — |
summary.warn |
integer ≥ 0, required | — |
summary.fail |
integer ≥ 0, required | — |
summary.unknown |
integer ≥ 0, required | — |
setup¶
Each step has a name, a status (ok, done, would, warn, manual, unknown,
fail) and a detail, and next lists what is left to do, in order. No secret ever
appears in it. See Set up a hub's platform.
| Key | Type | Description |
|---|---|---|
schema |
setup/v1, required |
Report format: setup/v1. |
command |
setup, required |
— |
engine |
string, required | touchmark's version. |
platform |
one of github, gitlab, required |
The platform set up. |
host |
string, required | The platform's host, with a port that is not the scheme's default. |
hub |
string, required | The hub repository's path on the platform. |
hub_id |
string | The hub repository's immutable id, the second half of its fingerprint. |
group |
string | GitLab: the full path of the group whose projects are the targets. |
org |
string | GitHub: the account that owns the hub and the Apps. |
isolation |
one of platform, external, required |
security.write_isolation of hub.yml: setup sets up isolated write keys only. |
dry_run |
boolean, required | The run had --dry-run: it read the platform and changed nothing; would steps say what it would change. |
accounts |
one of service-account, group-access-token, app |
What the reader and the writer are: GitLab service accounts or group access tokens, GitHub Apps. |
reader |
string | The reader's login, when known. |
writer |
string | The writer's login, when known: hub.yml's writer must name it. |
steps |
list of objects, required | The steps in the order they ran. Steps named "check <name>" are the checks of doctor --hub-token on where the hub keeps its keys, made after the changes. |
steps[].step |
string, not empty, required | The step's name, such as writer-variable or check key-location. |
steps[].status |
one of ok, done, would, warn, manual, unknown, fail, required |
ok: as wanted, nothing written; done: changed in this run; would: --dry-run would change it; warn: as wanted, with a caveat; manual: left to the person (run setup again once done); unknown: could not be read; fail: failed, or found what setup must not fix. |
steps[].detail |
string | — |
steps[].commands |
list of strings | Shell commands the person runs to finish the step, such as gh secret set for a GitHub App's private key. |
next |
list of strings, required | What the person does after this run, in order. |
summary |
object, required | Steps per status, every status present. |
summary.ok |
integer ≥ 0, required | — |
summary.done |
integer ≥ 0, required | — |
summary.would |
integer ≥ 0, required | — |
summary.warn |
integer ≥ 0, required | — |
summary.manual |
integer ≥ 0, required | — |
summary.unknown |
integer ≥ 0, required | — |
summary.fail |
integer ≥ 0, required | — |
status and apply¶
--format json prints the hub (its directory, id and commit), the target (its
directory, its reference in targets.yml, its opt-in file, whether it opted in, and
opt_in, which says why: file, the opt-in file is there; assumed, there is none and
a repo: entry with opt_in: assumed subscribes it; flag, there is none and
--assume-opt-in counts it as opted in; opted-out, the file says
enabled: false; none, neither), the
pack selection and where each pack came from, one entry per managed path with its
state, action, pack and blob ids, a summary counting each state, and warnings:
{
"schema": "status/v1",
"command": "status",
"dry_run": false,
"hub": { "dir": "/src/engineering-assets", "id": "acme-eng", "commit": "ffae0953c140298cf045e600f15710104e9d5d0d" },
"target": { "root": "/src/billing", "ref": "acme/billing", "opt_in_file": ".engineering-assets.yml", "opted_in": true,
"opt_in": "file" },
"selection": { "packs": ["agents"], "complete": true, "unresolved": [], "sources": { "agents": ["defaults"] } },
"entries": [
{ "path": "AGENTS.md", "state": "local", "action": "keep", "pack": "agents",
"from": "", "to": "", "mode": "", "detail": "", "after_deletes": false }
],
"summary": { "missing": 0, "current": 1, "outdated": 0, "local": 1, "ignored": 0, "retired": 0,
"retired_local": 0, "unsafe": 0, "orphaned": 0, "changes": 0, "done": 0, "skipped": 0, "failed": 0 },
"warnings": []
}
apply adds results, one per entry whose action is not keep, with its outcome
(done, skipped, failed); apply --dry-run has none.
| Key | Type | Description |
|---|---|---|
schema |
status/v1, required |
Report format: status/v1. |
command |
one of status, apply, required |
— |
dry_run |
boolean, required | apply --dry-run: the plan only, nothing written. Always false for status. |
hub |
object, required | The hub the report was made from. |
hub.dir |
string, required | The hub's directory. |
hub.id |
string, required | hub.yml's id; empty for a hub without hub.yml. |
hub.commit |
string, required | The hub commit read; empty when touchmark stopped before reading it. |
target |
object, required | The target working tree. |
target.root |
string, required | The target's working tree. |
target.ref |
string, required | The target in targets.yml, path or provider:path; empty when it could not be determined. |
target.opt_in_file |
string, required | The opt-in file's path in the target. |
target.opted_in |
boolean, required | The target gets packs: opt_in is file, assumed or flag. |
target.opt_in |
one of file, assumed, flag, opted-out, none, required |
Why the target is opted in or not. file: the opt-in file is there; assumed: there is none, and a repo: entry with opt_in: assumed subscribes it; flag: there is none, and --assume-opt-in counts it as opted in; opted-out: the file says enabled: false; none: neither. |
selection |
object, required | The resolved pack list; null when the target has not opted in. |
selection.packs |
list of strings, required | The packs the target gets, requires included. |
selection.complete |
boolean, required | Every pack named was found in the hub. |
selection.unresolved |
list of strings, required | Packs named that the hub does not have. |
selection.sources |
map of name to list of strings, required | Per pack, where it was selected: the opt-in file, targets.yml, defaults or requires <pack>. |
entries |
list of objects, required | The whole plan in plan order, one entry per managed path, current entries included. |
entries[].path |
string, not empty, required | The path in the target. |
entries[].state |
one of missing, current, outdated, local, ignored, retired, retired-local, unsafe, orphaned, required |
The file's state in the target. |
entries[].action |
one of keep, create, update, delete, adopt, chmod, required |
What apply does with it. |
entries[].pack |
string, required | The pack that ships the path, or shipped it. |
entries[].from |
string, required | The blob id the target has; empty when it has none. |
entries[].to |
string, required | The blob id apply writes; empty for keep and delete. |
entries[].mode |
one of `,100644,100755`, required |
The file mode apply writes; empty when it writes nothing. |
entries[].detail |
string, required | Why, for states and actions that need a reason. |
entries[].after_deletes |
boolean, required | Written after the deletes, because a path it needs is deleted first. |
results |
list of objects | apply without --dry-run only: what it did with each entry whose action is not keep, in execution order. |
results[].path |
string, not empty, required | — |
results[].action |
one of keep, create, update, delete, adopt, chmod, required |
— |
results[].state |
one of missing, current, outdated, local, ignored, retired, retired-local, unsafe, orphaned, required |
— |
results[].outcome |
one of done, skipped, failed, required |
— |
results[].detail |
string, required | The reason for skipped and the error for failed. |
summary |
object, required | Entries per state, the changes (entries whose action is not keep) and, for apply, the outcomes. |
summary.missing |
integer ≥ 0, required | — |
summary.current |
integer ≥ 0, required | — |
summary.outdated |
integer ≥ 0, required | — |
summary.local |
integer ≥ 0, required | — |
summary.ignored |
integer ≥ 0, required | — |
summary.retired |
integer ≥ 0, required | — |
summary.retired_local |
integer ≥ 0, required | — |
summary.unsafe |
integer ≥ 0, required | — |
summary.orphaned |
integer ≥ 0, required | — |
summary.changes |
integer ≥ 0, required | — |
summary.done |
integer ≥ 0, required | — |
summary.skipped |
integer ≥ 0, required | — |
summary.failed |
integer ≥ 0, required | — |
warnings |
list of strings, required | — |
check¶
--format json prints the hub, its packs, and every error and warning; errors is empty
when the hub passes.
| Key | Type | Description |
|---|---|---|
schema |
check/v1, required |
Report format: check/v1. |
command |
check, required |
— |
hub |
object, required | The hub checked. |
hub.dir |
string, required | The hub's directory. |
hub.id |
string, required | hub.yml's id; empty for a hub without hub.yml. |
hub.commit |
string, required | The hub commit read; empty when touchmark stopped before reading it. |
packs |
list of strings, required | The hub's packs. |
errors |
list of strings, required | What makes check exit 2; empty when the hub passes. |
warnings |
list of strings, required | — |