Repository navigation
Releases: mozership/probe
Release list
v1.25.0
Highlights
- A job file run by the
embeddedaction uses the names the workflow gives its external actions underactions. - A
usesthat names no action stops the run before its first job, and says which. This changes the exit status of such a run from 3 to 2. - The
dnsaction waits for a server as long as itstimeout, not 2 seconds. - For library users:
actionrpc.Call.Actions,(*Job).UseActions,WithActions,WithBuiltinActionsand their counterparts inembeddedare new. Nothing breaks.
Names of external actions in embedded job files
v1.24.0 let a workflow name its external actions once, under actions. A job file run by the embedded action did not see the names, and had to name each action in full. It is now read by the names of the workflow that embeds it, in uses and in the keys of defaults:
# workflow.yml
actions:
redis: github.com/mozership/probe-redis@7d60e0699a914e3c987ed5f2403ed8a7f3d176fa # v0.1.0
jobs:
- name: Session
steps:
- name: Check the session store
uses: embedded
with:
path: "./jobs/session.yml"
test: res.code == 0# jobs/session.yml
name: Session store
defaults:
redis:
url: redis://localhost:6379
steps:
- name: The store answers
uses: redis
with:
commands: [PING]
test: res.results[0] == "PONG"- The commit is written once, in the workflow, for the job files it embeds as well. The job file has no
actionsof its own, so one embedded by two workflows runs the action each of them names. - A job file embedded by a job file is read by the same names.
- A name that stands for a local path, as
greet: ./greetdoes, stands for the directory next to the workflow file, wherever the job file is. - Under a guard,
--allow-actiontakes the action in full, not the name. For a name that stands for a local path, that is the absolute path of the directory in a job file. probe checkreads the job file of an embedded step, and the ones that job embeds, when thepathis not a template. It reports ausesthat names no action, and an external action named wrongly, on the line of thepath. It did not read job files before, so a job file with a misspelt action is now a finding.
A uses that names no action stops the run
A uses that is not an external action was taken for an action of Probe and started as one. A name that is none, such as uses: shel, failed its step with the error of a plugin that did not start, which listed causes that were not the cause. In a job file run by embedded, the name was not shown at all.
A run now checks every uses before its first job, and stops with exit status 2:
[ERROR] configuration error in resolve_action: failed to resolve an action (caused by: job "first", step 1: unknown action "shel": it is not an action of Probe, an external action, or a name given under actions; did you mean "shell"?)
- The steps of the workflow are checked, and those of the job files its steps embed, when the
pathis written as it is or reads only the workflow'svars. - A job file whose path only the run knows, such as one read from
outputs, is checked as theembeddedaction starts the job. The step that embeds it fails with the same message. - A path made from a declared secret is hidden in the error, as
<secret:NAME>.
What changes for a workflow that has such a uses:
- It failed only when the step ran. It now stops before anything runs, even when the step would be skipped.
- Its exit status is 2, a configuration error, where it was 3, an action error.
probe check already reported an unknown action of a workflow, and still does.
dns waits as long as its timeout
The dns action of v1.24.0 gave up on a server that does not answer after 2 seconds, whatever its timeout: with the default 5s, or timeout: 30s, a step failed after 2 seconds with no answer from the DNS server, and did not say it had timed out.
- A step with
server, or a machine with one resolver, waits as long astimeoutsays, and fails withtimed out after <timeout>. - Without
server, each resolver of the machine is given an equal part of the time that is left, so that one that does not answer leaves time for the next. With two resolvers and5s, the first has 2.5 seconds.
See DNS.
For library users
Nothing breaks in this release. New public API:
actionrpc.Call.Actions, the names the workflow gives its external actions, each with the action in full, which Probe tells every action.pb.RunRequesthas the fieldactionsfor it.(*Job).UseActions(names), which reads a job by such names, and theStandaloneOptionWithActions(names), which tells them to the steps of a jobRunStandaloneruns.- The
StandaloneOptionWithBuiltinActions(names), which stops a job before its first step for ausesthat is not an external action or one ofnames. embedded.WithActions,embedded.WithBuiltinActionsandembedded.ServeWithActions, by which theembeddedaction is given both.
Workflow.Start checks each uses against Config.Actions. With none given, nothing is checked and a run behaves as before.
Changelog
- 1493336 Check the job files a job file embeds, and the external actions they name
- 289ac56 Merge branch 'feat/embedded-action-names' into feat/unknown-action-before-run
- 03526af Merge branch 'main' of github.com:mozership/probe into feat/unknown-action-before-run
- b4b2314 Merge pull request #369 from mozership/feat/embedded-action-names
- 7aaef5d Merge pull request #370 from mozership/feat/unknown-action-before-run
- 13ca0fb Merge pull request #371 from mozership/fix/dns-timeout-race
- 73b53b6 Read a job the embedded action runs by the names of the workflow
- 65a941b Read embedded job files with the vars of the run, and mask the error
- 2bad2aa Stop a run before its first job for a uses that names no action
- b35a2a2 Take back what go fix rewrote in files this change touches
- 03cc2e3 Tell a DNS query that timed out as one when the connection finds it first
- 23e9544 Wait for a DNS server as long as the timeout of the step
v1.24.0
Highlights
- A workflow names its external actions once, under
actions, and its steps anddefaultsuse the name. - The
dnsaction asks a DNS server for the records of a name. - Probe hides the credentials of object storage, and the password in a
url. - The external actions redis and s3 have reference pages.
- For library users:
Config.Actions,Workflow.Actions,mask.HoldsURL,mask.Userinfoandexpr.ReplaceTemplatesare new. Nothing breaks.
actions
A step that runs an external action writes its repository and a 40-character commit in uses, and a job's defaults writes them again as a key. A workflow can now write them once:
actions:
graphql: github.com/mozership/probe-graphql@41e4ffa222db58c63c7169117c919e6d252bdbf9 # v0.2.0
jobs:
- name: Countries API
defaults:
graphql:
url: https://countries.trevorblades.com/graphql
steps:
- name: Look up Japan
uses: graphql
with:
query: '{ country(code: "JP") { capital } }'
test: res.data.country.capital == "Tokyo"- A newer release of an action is a change to one line.
- A name is letters, digits,
_and-, and starts with a letter or a digit. - A name cannot be that of an action of Probe, such as
httporshell: whatuses: httpruns does not depend on the workflow. A workflow that gives such a name fails to load. - The value names an external action as
usesdoes: a repository pinned to a full commit SHA, or a local path. It is not a template, and it cannot be another name. - A step can still name an action in full, and the
defaultswritten by the name apply to it too. --allow-actiontakes the action in full, not the name: a workflow cannot choose what a name the person running Probe has allowed stands for.probe checkreports a wrong name or value with its line.- The names belong to the workflow file. A job file run by the
embeddedaction does not see them, and names its external actions in full.
See actions and Naming an External Action.
dns
A built-in action that asks a DNS server for the records of a name, and returns what the server answered: that a name resolves to the addresses expected, that the MX, SPF, DKIM and DMARC records of a mail flow are in place, or that a change to a zone has reached a name server.
- name: Where the mail of example.com goes
uses: dns
with:
name: example.com
type: MX
test: res.rcode == "NOERROR" && res.answers[0].host == "mail.example.com"
- name: ns1 serves the new address
uses: dns
with:
name: api.example.com
server: ns1.example.com
test: res.authoritative && res.values == ["203.0.113.10"]typeisAby default, and any type DNS has butAXFRandIXFR. Withtype: PTR,namemay be an address.serveris the server to ask, and the resolvers of the machine without it.protocolisudp,tcp, ortlsfor DNS over TLS. An answer cut short over UDP is asked for again over TCP.res.answersholds the records, each withname,type,ttlanddata, and the parts of anMX,SRV,SOAorCAAby name.res.valuesholds thedataof the answers of the type asked for.- A server that answers with an error, such as
NXDOMAIN, is a result a test can check, withstatus1. - A query writes nothing, so the action runs under
--read-only. Under--allow-host, the server asked must be one the run allows.
See DNS.
Credentials passed to an action
Probe showed the value of a password or key_passphrase field of with, and the password in a dsn, as <redacted>. It now does so for more:
- The value of a
secret_access_keyorsession_tokenfield, at any depth ofwith. - The password in a
url, such asredis://user:password@host. The rest of the URL stays visible. - When a template in the user info of a
urlcannot be evaluated, as inredis://app:{{vars.pw}}@host, the error names the value but does not say why, since the template and the error may each quote the password. The error of a template elsewhere in aurlis shown.
A credential written straight into a step therefore does not appear in --verbose output or an action's log records, even when it is not listed in secrets. See secrets.
redis and s3 have reference pages
Two external actions are new, each at v0.1.0, with a reference page in English and Japanese that probe guide redis and probe guide s3 print.
- mozership/probe-redis runs commands on a Redis or Valkey server and returns their replies. See Redis.
- mozership/probe-s3 reads and writes objects in S3 and in storage that speaks its protocol. See S3.
actions:
redis: github.com/mozership/probe-redis@7d60e0699a914e3c987ed5f2403ed8a7f3d176fa # v0.1.0
s3: github.com/mozership/probe-s3@8d2a120ffe6f2860229f5b6171a3477ed0f07b6d # v0.1.0Both declare guard: [read-only, allow-host], so they run under a guard without --allow-action.
For library users
Nothing breaks in this release. New public API:
Workflow.Actions, the names a workflow gives its external actions.Loadreplaces each name by its action, so aStep.Usesread after it is the action in full.Config.Actions, the names of the built-in actions, which a name underactionsmust not be. With none given, a clash is not checked.mask.HoldsURL(key)andmask.Userinfo(rawURL), which tell a key whose value is a URL and find the user info of one.expr.ReplaceTemplates(s, repl), which returnsswithreplin place of each template.
github.com/miekg/dns is a new dependency, for the dns action.
Changelog
- ac92463 Add the dns action, which asks a DNS server for the records of a name
- a01e0a1 Document redis and s3 as external actions
- 011ef0d Hide the credentials of object storage, and the password in a url
- 159f5fb Hide why a template in the user info of a url could not be evaluated
- 11773a6 Let a workflow name its external actions under actions
- 577e652 Merge pull request #365 from mozership/feat/dns-action
- 78a3f2e Merge pull request #366 from mozership/fix/mask-action-credentials
- d1f8fdd Merge pull request #367 from mozership/feat/action-aliases
- f6829a0 Merge pull request #368 from mozership/docs/redis-s3-actions
- 45df550 Refuse a template in the action a name stands for
v1.23.0
Highlights
probe manifestwrites theaction.ymlof an external action: the one of a release, and the one to start a new action from.- The external action websocket has a reference page.
- Probe needs Go 1.26.6 again, not 1.27.
- For library users:
actionref.Releaseandactionref.Scaffoldare new. Nothing breaks.
probe manifest
An external action's action.yml holds the URL and the SHA-256 of each executable, so it has to be rewritten for every release. Each action did this with a shell script of its own, with the name, the description, guard and params written in the script. probe manifest does it for any action, from the action.yml itself:
probe manifest v0.2.0 dist/checksums.txt > "$RUNNER_TEMP/action.yml"
cp "$RUNNER_TEMP/action.yml" action.yml- It reads
action.yml, puts the tag inruns.urland the digests of the checksums file inruns.checksums, and prints the result.name,description,guardandparamsare printed as they were read, soaction.ymlis the one place an action is described in. - The checksums file is what
sha256sumor GoReleaser writes. A line is taken when its file is the assetruns.urlnames, with a platform in place of{os}and{arch}. runs.urlmust be a release asset on GitHub,https://github.com/<owner>/<repo>/releases/download/<tag>/<asset>, with a character such as_between{os}and{arch}.- The tag is letters, digits,
.,_,+and-. - The output is always written in the same form. The comment lines
action.ymlstarts with are kept, and no other comment is. - Do not redirect the output straight to
action.yml: the shell empties it before Probe reads it.
For a new action, probe manifest init prints the action.yml to start from:
probe manifest init mozership/probe-greet > action.ymlname: greet
# What the action does, in a line.
description: ""
# guard: [read-only, allow-host]
# params: [name]
runs:
using: binary
url: https://github.com/mozership/probe-greet/releases/download/v0.0.0/probe-greet_{os}_{arch}- The action is named after the repository, without a leading
probe-, and the executable of each platform<repo>_{os}_{arch}, as GoReleaser names one it publishes unarchived. guardandparamsare commented examples to fill in, each with a comment that says what it is for (shortened above).- The tag is a placeholder and there are no checksums: the first release puts both in.
See manifest and Writing an External Action.
websocket has a reference page
mozership/probe-websocket v0.1.0 is an external action that connects to a WebSocket server, sends and receives messages in the order a step lists them, and returns those it received. It now has a reference page, in English and Japanese, and probe guide websocket prints it.
uses: github.com/mozership/probe-websocket@a159244b9ab3f5ec4c61782af1e711bce705c8a8 # v0.1.0It declares guard: [read-only, allow-host], so it runs under a guard without --allow-action. See WebSocket.
Go 1.26.6
go.mod goes back to go 1.26.6. v1.20.0 raised it to 1.27 for chromedp, which left Probe with the browser action in v1.22.0. A module that imports probe no longer needs Go 1.27 for it.
Docs
- The README and the landing page say what Probe is for, who writes a workflow and who runs it, and what Probe does for an AI coding agent.
- Understanding Probe shows the architecture diagram, drawn for the light and the dark theme.
For library users
Nothing breaks in this release. New public API in actionref:
Release(manifest, tag, checksums), whichprobe manifest <tag>prints the result of.Scaffold(repo), whichprobe manifest initprints the result of.
Changelog
- 3bed050 Add probe manifest to write the action.yml of an external action
- 786e84b Build the Features around why Probe was made and three strengths
- 89269e3 Document websocket as an external action
- 560f521 Merge pull request #358 from mozership/chore/go-1.26
- 06b01f4 Merge pull request #359 from mozership/chore/go-fix
- 8695aab Merge pull request #360 from mozership/docs/architecture-diagram
- 6a774b4 Merge pull request #361 from mozership/docs/readme-use-cases
- 72f2020 Merge pull request #362 from mozership/docs/lp-lede-agents
- 55b879c Merge pull request #363 from mozership/docs/websocket-action
- 2b8108c Merge pull request #364 from mozership/feat/manifest-command
- 45766ac Need Go 1.26.6 again, now that chromedp is gone
- 07070d0 Open the README with the three strengths, in a sentence each
- 1f3d4fd Refuse a tag or an asset name that probe manifest cannot write right
- 97bc885 Run go fix in CI, and apply what it rewrites
- 2e0c59c Say in the README how the results of a run can be read
- db742c4 Say in the README what the recent features are for
- 2cf682f Say on the landing page what Probe does for an AI coding agent
- 9ab2e89 Say who writes the workflow and who runs it
- 3cd2825 Show the architecture diagram in Understanding Probe, drawn for each theme
v1.22.0
Highlights
- The built-in
browserandmail-latencyactions are now external actions, mozership/probe-browser and mozership/probe-mail-latency. A workflow that uses either has to name it by repository and commit. mail-latencynow runs under--allow-hostwithout--allow-action.- The docs pin graphql and jmap at v0.2.0, which keep to
--read-onlyand--allow-hostand declare the keys ofwiththey take. - Probe no longer depends on chromedp.
- For library users: the packages of the two actions moved to their repositories.
browser and mail-latency are external actions
The two actions left Probe for repositories of their own, with the same with and the same result. A step names each one by repository and commit in place of the built-in name:
# Before
- uses: browser
- uses: mail-latency
# After
- uses: github.com/mozership/probe-browser@42dda412d5d183c091f8baf071237cc090f16052 # v0.1.0
- uses: github.com/mozership/probe-mail-latency@2340503ddb7081e1a7e4cc7e4391ac6e5937574d # v0.1.0uses: browseranduses: mail-latencyare now unknown actions, andprobe checkreports them.- Probe downloads each executable the first time a workflow uses it, and checks it against the SHA-256 in the
action.ymlat the pinned commit. Each release's notes start with theusesline to copy. - Their
action.ymldeclaresparams, soprobe checkreports a key ofwiththe action does not take. - browser declares no guard, as it cannot tell what a page will do: under
--read-onlyor--allow-host, it still needs--allow-action. mail-latency declaresallow-host, as it connects to no host, so it now runs under--allow-hostwithout--allow-action. It writes its CSV, so it does not declareread-only. - The built-in actions are now
http,db,grpc,shell,ssh,smtp,imap,embeddedandhello.
See Browser and Mail Latency.
graphql and jmap v0.2.0
The docs, the example and the e2e pin the external actions at v0.2.0:
uses: github.com/mozership/probe-graphql@41e4ffa222db58c63c7169117c919e6d252bdbf9 # v0.2.0
uses: github.com/mozership/probe-jmap@52000030f1bd839677830d1ab07a024183f581b7 # v0.2.0- Both declare
guard: [read-only, allow-host], so they run under a guard without--allow-action. graphql keeps to it from this release: under--read-onlyit sends only a query, read with a GraphQL parser, and under--allow-hostit checks the host ofurland of each redirect. - Both declare
params, and fail a step whosewithhas a key they do not take.
For library users
The packages moved with the actions, with no alias left:
| Before | After |
|---|---|
github.com/linyows/probe/actions/browser |
github.com/mozership/probe-browser/browser |
github.com/linyows/probe/actions/mail-latency (package maillatency) |
github.com/mozership/probe-mail-latency/maillatency |
actions.Names, actions.Params and actions.AllParams no longer list browser or mail-latency, and a module importing probe no longer pulls in chromedp. See CHANGELOG.md.
Changelog
- ce65807 Date v1.22.0 the day it is released
- 768748b Merge pull request #354 from mozership/chore/external-actions-v0.2.0
- a06e99f Merge pull request #355 from mozership/feat/browser-external
- f5f328f Merge pull request #356 from mozership/feat/mail-latency-external
- fcb3d89 Merge pull request #357 from mozership/release/v1.22.0
- 93165b1 Move the browser action out of Probe, to mozership/probe-browser
- 8fa551a Move the mail-latency action out of Probe, to mozership/probe-mail-latency
- 610cb5e Pin probe-graphql and probe-jmap v0.2.0, which declare the guard they keep
- 1ef7454 Prepare the changelog for v1.22.0
v1.21.0
Highlights
grpccalls methods with the Connect protocol, and calls streaming methods.grpckeeps to--read-onlyand--allow-host.- Each action declares the kinds of guard it keeps to. An external action does so in
action.yml, which also takesparamsforprobe check. - The external actions graphql and jmap have reference pages.
- For library users:
actionrpc.Guard.Keepingis replaced byKeeps.
gRPC over the Connect protocol
The grpc action can now call a method with the Connect protocol instead of gRPC. The call is a POST over HTTP, so it passes an HTTP/1.1 proxy or load balancer, as the calls of a connect-web client in a browser do.
- name: Get a user
uses: grpc
with:
protocol: connect
addr: https://api.example.com/rpc
service: users.v1.UserService
method: GetUser
body:
id: "123"
test: res.status_code == "OK" && res.body.user.name != ""addris the URL the service is served under; the call goes to<addr>/<service>/<method>. A host and port without a scheme is taken ashttp, orhttpswithtls: true.codecisjson(the default) orproto. Withoutprotofiles, a JSON body is sent as written andservicemust be the full name;codec: protoneedsproto.metadatais sent as headers, andtimeoutis also sent asConnect-Timeout-Ms. Redirects are not followed.reshas the same fields as for a gRPC call. A Connect error gives its gRPC code, such asNOT_FOUND; a response without one, such as a proxy's 502 page, gives the code implied by the HTTP status, such asUNAVAILABLE, as a result rather than an action error.- Server reflection is not used with
protocol: connect.
See Connect Protocol.
gRPC streaming methods
The grpc action now calls methods that stream their requests or their responses, by gRPC and by protocol: connect, with the same parameters as a method that does not stream. The call sends every request, closes its side, and then reads the responses.
- name: Watch users
uses: grpc
with:
method: WatchUsers
body:
filter: active
max_messages: 3
test: res.status_code == "" && len(res.messages) == 3 && !res.complete
- name: Import users
uses: grpc
with:
method: ImportUsers
body:
- name: Ada
- name: Grace
test: res.status_code == "OK" && res.body.imported == 2- A list in
bodyis the messages of a stream of requests, in order. One object is the only message, and[]sends none. A list sent to a method that does not stream its requests is an error. - The responses of a stream are in
res.messages, the last one also inres.body.res.completeistruewhen the server ended the stream. max_messagesstops reading after that many responses. The server has then sent no status, sores.status_codeis empty andres.completeisfalse.- A stream that breaks off after some responses returns them with the status it ended with, such as
DEADLINE_EXCEEDED. - A method that streams both ways is refused before anything is sent.
- With
protocol: connect, a streaming method is called only whenprotofiles declare it. Withproto, each message is checked, and a violation names it by index, such as$[1].user.email.
See Streaming Methods.
The grpc action keeps to the guard
The grpc action now runs under --read-only and --allow-host without --allow-action grpc. It refuses a call the guard does not allow before anything is sent, and the step fails with the kind refused.
--allow-hostis checked against the host and port ofaddr, port 443 when none is named.dns:///andpassthrough:///targets are read through, and the DNS server of adns://server/target is checked too, port 53 when none is named. A target that names no host, such asunix:///tmp/grpc.sock, is refused. Withprotocol: connect, the host of the URL is checked, at the port of its scheme when none is named.- Under
--read-only, a method is called only when every definition of it at hand, the server's reflection and the.protofiles ofproto, declaresoption idempotency_level = NO_SIDE_EFFECTS;. A Connect call withoutprotohas no definition and is refused. - The action applies these checks even when
--allow-action grpcis given. Under--read-only, a workflow that ran every grpc step with--allow-action grpcin v1.20.0 now has calls to methods not declaredNO_SIDE_EFFECTSrefused.
See Under a Guard.
Actions declare the guard they keep to
Which actions run under --read-only and --allow-host used to be a fixed list of built-in names in Probe, so an external action was always refused under a guard unless --allow-action named it. Each action now declares the kinds of guard it keeps to, and an external action does so with guard in its action.yml:
name: greet
description: Say hello over HTTP
guard: [read-only, allow-host]
params: [url, name]
runs:
using: binary
url: https://github.com/<owner>/probe-greet/releases/download/v0.1.0/probe-greet_{os}_{arch}
checksums:
linux_amd64: <SHA-256 of probe-greet_linux_amd64>- The kinds are
read-onlyandallow-host. A step runs under a guard only when its action declares every kind the run is under, or when--allow-actionnames it. An unknown kind is ignored. - Probe does not verify a declaration. The guard holds only as far as the external actions a workflow uses keep to what they declare, so review them before running a workflow under a guard.
- The built-in actions keep to the guard as before:
http,db,grpc,embeddedandhellodeclare both kinds. - The refusal now names the missing kinds, such as
does not keep to --read-only and --allow-host, which the run is under. - Probe reads
action.ymlof every external action before the first job, and downloads the executable only for one that the guard lets run. paramslists the keys the action takes inwith.probe checkreports a key that is not listed, in a step'swithand in a job'sdefaults. Withoutparams,withis not checked. Anaction.ymlthatprobe checkcannot read gives a warning.
The new Guard page describes what Probe and the actions each do, the table of what each built-in action refuses (moved from the CLI reference), and how an external action keeps to the guard. The action.yml fields are in External Actions.
Reference pages for graphql and jmap
The external actions graphql and jmap now have reference pages, in English and Japanese, and probe guide graphql and probe guide jmap print them. Both repositories moved to mozership; the docs and examples now pin these:
uses: github.com/mozership/probe-graphql@ad456d1eefd30a63d14b49c730e5239c4749b34b # v0.1.1
uses: github.com/mozership/probe-jmap@295701f5263fffd64161a34dd377722ccad3923a # v0.1.0- probe-graphql v0.1.1 is the same action as v0.1.0, released from mozership/probe-graphql, and its
action.ymldownloads the executables from there. - The
action.ymlof neither release declaresguardyet, so both are still refused under a guard unless--allow-actionnames them. probe-jmap refuses what the guard does not allow when it is told the guard.
For library users
This release breaks the Go API; workflow files are not affected. actionrpc.Guard.Keeping []string is replaced by Keeps map[string][]string, the kinds of guard each action keeps to by its uses, and a Guard built with Keeping: actions.Keeping() is now built with Keeps: actions.Keeps(). Guard.Runs now requires every kind in Guard.Kinds(). In pb.Guard, field 4 is reserved and keeps is field 5. An external action built with an earlier actionrpc still reads the guard it is sent. See CHANGELOG.md for the full table.
New public API:
actionrpc:Guard.Kinds,Guard.Missing,Guard.WithKeeps, and the constantsKindReadOnlyandKindAllowHost.actions.Keeps(), and aKeeps()in each ofdb,embedded,grpc,helloandhttp.actions.Keeping()stays, computed from the declarations.actionref.ManifestgainsGuardandParams;Resolver.Manifestandactionref.ReadManifestread anaction.ymlwithout fetching the executable.probe.CheckOptions.Manifest, whichprobe checksets toactionref.ReadManifestto check an external action'swithagainst itsparams.grpc.RequestStep, andRunStepon the grpc action, which make a call under the guard of the run;grpc.Requeststays and callsRequestStepwithout a guard.
Changelog
- 9245df5 Call a gRPC method with the Connect protocol
- c5bd19d Call streaming methods with gRPC and the Connect protocol
- 5212a1a Check an addr without a scheme as the URL it stands for
- dc37b8e Check the DNS server a dns:// target names against the guard
- a4640a5 Close the connection, keep the timeout header in range, read a reply as JSON
- 446f932 Document graphql and jmap as external actions, apart from the built-in ones
- 2f48226 Keep the grpc action to the guard of a run
- d767575 Let each action declare the guard it keeps to, and document the guard
- abefbbb Merge pull request #347 from mozership/feat/grpc-...
v1.20.0
Highlights
httpchecks requests and responses against an OpenAPI document withopenapi, andgrpcchecks calls against.protofiles withproto.probe coveragelists the operations or methods of a document that no step checked.probe checkreports mistakes in a workflow without running it.--read-only,--allow-hostand--allow-actionlimit what a run may do.- Building Probe, or a module that imports it, needs Go 1.27.
Checking HTTP calls against an OpenAPI document
The http action can now check each request and response against an OpenAPI document, independently of the step's test. Set openapi in a job's defaults to check every request of the job:
- name: Users
defaults:
http:
url: "{{vars.api_url}}"
openapi:
spec: ./openapi.yml
steps:
- name: Create a user
uses: http
with:
post: /users
body:
name: probe
test: res.body.name == "probe"- A request that breaks the document fails the step with the new kind
contract_request: a missing or invalid parameter, a body that does not match its schema orContent-Type, or missing credentials thatsecurityrequires. - A response that breaks it fails with
contract_response: no operation for the method and path, an undeclared status code, aContent-Typethe response does not declare, or a header or body that does not match its schema. - The step fails even when its
testholds. A step without atestpasses when the document allowed the request and the response. request: falsechecks the response only, for a step that sends an invalid request on purpose.strict: truealso fails on undeclared properties, undeclared query parameters,readOnlyproperties in a request andwriteOnlyproperties in a response.openapi: falseturns the check off for one step.- Violations are in
res.violationsand in the terminal output and every report format. Both kinds exit with status 1.
Checking gRPC calls against .proto files
The grpc action can check a call against .proto files, such as those in the repository the service is built from, instead of trusting only the definition the server reports through reflection. The files are compiled inside Probe, so protoc is not needed.
defaults:
grpc:
addr: localhost:50051
service: user.v1.UserService
proto:
files: [./proto/user/v1/user.proto]
import_paths: [./proto]- The request body is checked against the files' request message (
contract_request). The server's definition is compared with the files' field by field, and the response bytes are decoded again with the files' response message (contract_response). buf.validate(protovalidate) rules are checked on the request and the response.google.api.field_behavioris checked as AIP-203 defines it: aREQUIREDfield must be in the request, and anINPUT_ONLYfield must not be in the response. Both.protofiles are built into Probe, so they need not be in an import path.- A server without reflection can now be called: when
protois given and reflection is off, the call uses the files' definition. request: false,strict: trueandproto: falsework as they do foropenapi.
See Checking Against .proto Files.
probe coverage
probe coverage reads the JSON report of a run and lists which operations and responses of an OpenAPI document, or which methods of a .proto file, no step checked.
probe --report json workflow.yml
probe coverage openapi.yml probe-report.json
probe coverage proto/users.proto probe-report.json- A step counts when its call was checked against the document, whether the step passed or failed.
- It exits with 0 whatever the coverage, and with 2 when the document or the report cannot be read, or when no step in the report was checked against the document given.
- The JSON report now records what each step was matched to as
contract, and the http and grpc actions return it inres.contract.
See coverage.
probe check
probe check reports mistakes in a workflow without running it. A run ignores keys it does not recognize, so a misspelled test leaves a step unchecked and the run still passes; probe check reports it with the file and line.
workflow.yml:10: error: job 0 "Users", step 0 "Log in": unknown key "tset"; did you mean "test"?
workflow.yml:14: error: job 0 "Users", step 1 "Read": unknown action "htp"; did you mean "http"?
workflow.yml:18: error: job 0 "Users", step 1 "Read": with: outputs.login.tokn is not published: step "login" publishes token
- Errors: unknown keys in a workflow, job, step,
retry,repeat, a built-in action'swith(such asbdoyforbody) or a job'sdefaults; unknown actions; badneedsand step ids; expressions and templates that do not parse; outputs that no step publishes or that are read before their step publishes them. - Warnings: an output read from a job the reading job does not need, a step that nothing checks, and a
testthat reads nothing, such as1 == 1. - It exits with 2 when it finds an error, and with 0 otherwise. The skill installed by
probe skillnow tells coding agents to run it before running a workflow they wrote.
See check.
Running under a guard
Three new flags limit what a run may do. They are set on the command line or in the environment by whoever runs Probe, and a workflow cannot override them.
probe --read-only --allow-host api.staging.example.com,*.internal workflow.yml
probe --read-only --allow-action shell workflow.yml--read-onlyletshttpsend onlyGET,HEADandOPTIONS, and letsdbrun only a singleSELECT,SHOW,DESCRIBE,DESC,EXPLAINorWITHstatement inside a read-only transaction.--allow-hostrefuses connections fromhttpanddbto hosts not in the list. Fordb, the host checked is the one the driver itself reads from the DSN.http,db,embeddedandhellofollow the guard. Every other action, built-in or external, is refused under a guard unless--allow-actionnames it. A job run byembeddedruns under the same guard.- A refused step fails before anything is sent, with the new kind
refused; it is not retried and exits with status 2. - The environment variables are
PROBE_READ_ONLY,PROBE_ALLOW_HOSTSandPROBE_ALLOW_ACTIONS. A flag given on the command line takes precedence, so--read-only=falseor--allow-action=clears what the environment set. - The guard is not a sandbox: it limits what the listed actions send, not what a process can do.
See --read-only.
Other changes
- The browser action is built on chromedp v0.19.1, which talks to Chrome over a pipe instead of a websocket, so browser steps run faster. A step that waits for an element present on both the current and the next page, such as
h1right aftersubmit, may now read the page before the navigation finishes; wait for an element only the next page has. - Installing with
go installneeds Go 1.27 or later. - The skill installed by
probe skillnow says that templates are evaluated in keys at any depth ofwithand of a map undervars, but not in the name of a var itself.
For library users
This release breaks the Go API in a minor version. A module that imports probe needs Go 1.27 or later. browser.BrowserRunner.Run and the function fields and recorded calls of browser.MockRunner take chromedp.Action[chromedp.Void] instead of chromedp.Action, following chromedp's change to a generic action type. Workflow files are not affected. See the CHANGELOG for the full list of signatures.
New public API:
probe.Check,FindingandCheckOptionsrun the checks ofprobe check.actions.Paramsandactions.AllParamsgive the keys each built-in action takes, to pass asCheckOptions.Params.probe.Config.Guardsets the guard of a run, andprobe.WithGuardthe guard of a job run withRunStandalone, whoseJobRun.Refusedholds the first refusal.actionrpcgainsGuard,Refused,RefuseandIsRefusedfor actions that follow it, andactions.Keepinglists the built-in actions that do.oas.NewCoverage,oas.ReadReportandoas.CountCoveragecompute coverage, andactions/grpc.NewCoveragedoes so for a.protofile.reportgainsContract,Violation,FailureContractRequestandFailureContractResponse, andprobegainsFailureRefused.
Changelog
- 115fdf9 Add a section for coding agents to the landing page
- f8df780 Build reflected definitions with their imports, and read replies apart
- 5971c8b Bump grpc, mysql, validator and protovalidate
- 7e01650 Call a server without reflection with the definition of the .proto files
- 43dd3b5 Call actions actions rather than plugins in the docs
- 73989ad Carry what each step's response was matched to into the JSON report
- 241c56d Check an empty body, and take a name starting with two dots as under its path
- 1c2a7ed Check bodies other than JSON, and empty or null JSON bodies
- 08bb165 Check gRPC calls against .proto files with proto
- 57724fd Check http responses against an OpenAPI document with openapi
- 039dfb5 Check keys from...
v1.19.0
Highlights
httpsends forms and uploads files, withformandmultipart.httpkeeps cookies across the steps of a job, withkeep_cookies,cookiesandres.cookies.file()andtemplate()read request bodies and expected responses from files.trace_headersends a header that names the run, job and step of each request.- A
{{ }}in a map key is now evaluated. This can change a workflow that had one.
Forms and file uploads
The http action can now send a form and upload files. form sends an application/x-www-form-urlencoded body, and multipart sends text fields and files as multipart/form-data:
- name: Upload an avatar
uses: http
with:
post: /api/images
multipart:
title: avatar
image:
file: ./fixtures/logo.png
attachments:
- file: ./fixtures/terms.pdf
- content: "a,b\n1,2\n"
filename: data.csv
content_type: text/csv
test: res.code == 201- In
multipart, a string, number or boolean is a text field, and a map withfileorcontentis a file. A list sends the field once per value.filepaths are relative to the working directory. filenamedefaults to the base name offile, or the field name forcontent.content_typedefaults to the type the extension names, orapplication/octet-stream.- Text fields are sent before files, each sorted by name, since some servers (such as S3 for a POST upload) read only the fields before the file. The body is sent with a Content-Length.
- Either option sets the
Content-Typeheader, replacing one fromheadersordefaults. Givingform,multipartandbodyin any combination is an error before the request is sent. - A multipart body is not shown:
req.bodyis empty andreq.multipartshows what was sent. A form body is shown inreq.body.
See Sending a Form and Uploading Files.
Cookies across steps
Every http request now has a cookie jar, and keep_cookies: true keeps the cookies a server sets for the following steps of the job:
- name: Logged-in pages
defaults:
http:
url: "{{vars.api_url}}"
keep_cookies: true
steps:
- name: Log in
uses: http
with:
post: /login
form:
user: "{{vars.user}}"
password: "{{vars.password}}"
test: res.code == 200 && res.cookies.session != ""
- name: My page
uses: http
with:
get: /me
test: res.code == 200- A cookie set on a redirect is now sent on to where the redirect leads. It used to be dropped. A
cookieheader inheadersis still sent. res.cookiesholds the cookies the server set, by name, including those set on redirects.- Kept cookies are sent according to their domain, path, expiry and
Secureflag. A cookie the server removes or that expires is no longer sent. Each job keeps its own cookies, and each run of a repeated job, and a job run byembedded, starts with none. cookiessends cookies by name, with or withoutkeep_cookies, in place of a kept cookie of the same name. They go only to the host of the request, not to another host a redirect leads to.- The values of
cookiesandres.cookiesare hidden in the output. Their names are shown.
See Cookies.
Bodies and expected responses kept in files
Two new expression functions read a file and expand templates in it. file(path) returns a file's content as it is, and template(s) expands the {{ }} templates in a string:
- name: Create a user
uses: http
with:
post: /users
headers:
content-type: application/json
body: "{{ parse_json(template(file('fixtures/user.json.tmpl'))) }}"
test: match_json(res.body, parse_json(file('expected/user.json')))filepaths are relative to the working directory. A missing file, a directory or another non-regular file, and a file larger than 1000000 bytes are errors.filedoes not expand templates, so a file that holds{{as plain text can be sent as is. Pass it throughtemplateto expand them, against the samevars,outputsand other fields as the calling expression.- A template is replaced by its value as text, so a string that may hold a quote is better written with
toJSON, as in{"name": {{ toJSON(vars.name) }}}. templatecalls nest at most 10 levels deep, so a file that reads itself fails at once.- A var that calls
templateis evaluated after the vars that do not.
Trace header
The http action can send a header that names the run, job and step of each request, so that the request can be found in the server's access log. trace_header: true sends X-Probe-Trace, and a header name sends the same value under that name:
X-Probe-Trace: run=7f3a9c21e4b05d68; job=login; step=auth; repeat=0; attempt=1
stepis the step'sid, orstep_<index>for a step without one.repeatcounts from 0 andattemptfrom 1. Values are escaped as in a URL query.- The run ID is also written to the JSON report as
run_id. A job run byembeddedhas the run ID of the step that embeds it. - The header shows in
req.headers. Giving a header of the same name inheadersas well is an error. - The option is off by default. It is not named
trace, which stays the shorthand for the TRACE method.
See Tracing Requests.
Templates in map keys
A map key that holds {{ }} is now evaluated as a template, in with, vars and a step's vars, so a key can come from a value known only at run time:
body:
type: status
"{{ outputs.tenant.id }}":
active: true- A key is evaluated as text. Two keys that come to the same key are an error, as is a key that cannot be evaluated; the error names the key as written.
- This changes workflows that had
{{in a key: it used to be sent as written. - A var whose key reads another var is evaluated after it.
See Expressions and Templates.
Output and logging
- The percent-encoded form of a secret or credential is now hidden too, so a password sent in a
formbody does not show asp%40ss. - With
-v, the http action logs the method, URL and headers of each request and the status and headers of each response, with credentials hidden. It used to log only a warning that these could not be encoded.
For library users
Two narrow breaks, both in what was added since v1.18.0:
(*Job).RunStandalonetakes...StandaloneOptionafter its other parameters. Calls compile as before, but the method as a value has a new type, so code that assigns it to the old function type or an interface with the old signature no longer compiles.expr.Exprhas a function-typed field,BeforeTemplate, so it is no longer comparable with==or usable as a map key.
New public API:
actionrpc.StepActionlets an action know the step it runs for and keep state across the steps of a job.RunStep(call Call)receivesWith,StateandStep(run ID, job, step index, ID and name, repeat, attempt).actionrpc.RunStepandactionrpc.RunExecutableStepare the entry points, andStepActionRunnerandMockActionRunner.Calls/Statesare the runner side.Action,Run,RunExecutableandActionRunnerare unchanged, and the newstepandstatefields of the RPC messages are ignored by existing external actions.Workflow.RunID()returns the run ID, andWithRunIDpasses one toRunStandalone.http.RequestStepis the http request entry point that takes aCall.
See Knowing the Step and Keeping State and the CHANGELOG.
Changelog
- 1cbc9a6 Bound file reads and evaluate template vars after the others
- a7da753 Bound what template expands and evaluate the vars it reads first
- e587abb Escape line breaks in multipart names and keep the caller's headers
- 8109f18 Evaluate templates in the keys of maps
- d1a367d Hide errors under keys that become credentials, count every key
- bf477c7 Keep action state in the form it takes when it is sent
- ac03aa3 Keep cookies as they came and state as it would be sent
- fb80dbe Keep cookies in a job through state that actions keep
- 9120ac0 Key a list var by a name of its own and describe EvalTemplateMap
- 484c320 Log the http request and response with credentials hidden
- 17d322f Merge pull request #317 from linyows/feat/http-multipart-form
- cefe878 Merge pull request #318 from linyows/feat/action-state-cookies
- 1a6cc0d Merge pull request #319 from linyows/feat/file-template-functions
- b88e83b Merge pull request #320 from linyows/feat/trace-header
- 251756a Merge pull request #321 from linyows/feat/template-map-keys
- ce14bac Merge pull request #322 from linyows/release/v1.19.0
- db7e92b Move the state tests next to the tests of the code they test
- 080caec Name a step without an id as ...
v1.18.0
Highlights
- A template that cannot be evaluated now fails the step, instead of sending the error text to the action. This can change the result of an existing workflow.
- Workflow
varscan read other vars. - A background
shellstep waits until the command is ready, withready.log. httptakesbasic_auth, andshellandsshdecode JSON output intores.jsonand reportres.timed_out.- For library users: the packages of the built-in actions moved under
actions/.
A template that cannot be evaluated fails the step
A template that does not parse or fails while it runs used to be replaced by the error text, such as [RuntimeError: cannot fetch token from <nil>], and that text was sent to the action as a URL, a header or a command. Now the step fails before the action runs, and the error names the value and the template. This can change the result of an existing workflow.
headers:
# Fails the step when no earlier step published outputs.login
authorization: "Bearer {{outputs.login.token}}"
# Falls back instead
# authorization: "Bearer {{outputs.login?.token ?? 'none'}}"- In a step's
with,varsorname: the step fails with the new failure kindtemplate, and Probe exits with status 1. - In a workflow's
vars: the run stops before the first job with exit status 2. - In
echo: the error is shown in place of the text, and the step's result does not change. - A name that is not defined still reads as nil and is not an error, so
vars.none ?? 'x'works as before. - When the failing value is a credential, such as
with.password, the error names the value but leaves out the reason, since the template or the error could contain the credential.
See Expressions and Templates and the failure kinds in the CLI reference.
Workflow vars can read other vars
A var can read another var as vars.<name>, in any order within the vars block. Each var is evaluated once, after the vars it reads, so a random value is the same in every var built from it.
vars:
http_port: "{{HTTP_PORT ?? '18080'}}"
base: "http://localhost:{{vars.http_port}}"
password: "{{PASSWORD ?? random_str(24)}}"
basic_auth: "Basic {{encode_base64('alice:' + vars.password)}}"- A var that reads itself, directly or through other vars, stops the run before the first job with an error such as
vars: circular reference: a -> b -> a. - A list at the top level of
varsnow has its templates evaluated, as a list inside a map already did.
See vars.
Wait for a background command to be ready
A background shell step now takes ready.log, the text the command writes once it is ready. The step returns when that text appears in stdout or stderr, so the next step can use the server without retrying.
- name: Start the server
uses: shell
with:
cmd: python3 -u -m http.server 8080 --bind 127.0.0.1
background: true
ready:
log: Serving HTTP
test: status == -1- If the command exits first, the step fails with its exit code in
res.codeand its output inres.stdout. Iftimeout(default30s) passes first, the step fails withres.timed_outset totrue, and the command is stopped. - The text is matched as plain text, not as a pattern.
- A program that buffers its output when not writing to a terminal, such as Python, has to be told to write at once, as with
python3 -u.
See ready.
HTTP Basic authentication
The http action takes basic_auth with a username and a password and builds the Authorization header from them.
defaults:
http:
url: "{{vars.api_url}}"
basic_auth:
username: "{{vars.admin_user}}"
password: "{{vars.admin_password}}"- The request is refused before it is sent when the username is missing or contains a colon, when
basic_authhas another key, or when anauthorizationheader is also given. - The password and the header built from it are hidden in the output.
See the http action.
JSON output of a command as res.json
When the whole of a command's stdout is one JSON object or array, shell and ssh also decode it into res.json, so expressions no longer need parse_json(res.stdout).
test: res.code == 0 && res.json.count == 1
outputs:
id: res.json.emails[0].idres.stdoutstill holds the text. Any other output, such as plain text, a single number or JSON Lines, leavesres.jsonunset (nil).- This also applies to an external action that returns
stdout, unless it setsjsonitself.
See the shell action and the ssh action.
Command timeouts in shell and ssh
A command stopped at its timeout now sets res.timed_out to true and keeps what it wrote until then in res.stdout and res.stderr. It is still a result, not an action error.
shell:res.codeis-1. The timeout now holds when the command has started a process that keeps the output open, assleep 30 & echo donedoes; such a step used to wait for that process.ssh:res.codeis the status the command exits with afterSIGTERM. A command still running 5 seconds later givesres.code-1with the output read so far, where it used to be an action error without output.
Passwords hidden wherever Probe shows them
Credentials passed to an action are now hidden in --verbose output, in the request printed for a failed step and in the action's log records, even when they are not listed in secrets.
- The value of any
passwordorkey_passphrasefield at any depth ofwith, including a password written as a number. - The password in a database URL such as
postgres://app:secret@db/main, as written and percent-decoded. The rest of the URL stays visible. - A long token or password is hidden before an action's log shortens values to 200 bytes, so its first part no longer shows.
See secrets.
Fixes
probe daglooks for an embedded job file in the working directory, as the run does, instead of first looking next to the workflow file.- A job that could not start, because a step is invalid or its name cannot be evaluated, now prints why.
- A step in a job with
repeatkeeps why its first failing iteration failed, so a JSON report keeps the kind and JUnit reports an<error>for an action or template failure instead of a<failure>. - The details under a failed step are indented to the step's name.
- An action result holding a map whose keys are not strings, such as numbers, is now sent with the keys as text; keys that cannot be told apart that way fail the step instead of the entries being dropped.
- Every YAML example in the docs now parses, and expressions such as
filter(res.body.users, #.active == true)are quoted so that YAML does not read#as the start of a comment.
For library users
The Go API breaks in this minor version. The packages of the built-in actions moved under actions/, such as github.com/linyows/probe/http to github.com/linyows/probe/actions/http, with no aliases left. The mail package was split into actions/smtp and actions/mail-latency (package maillatency). expr.EvalTemplateMap now returns (map[string]any, error), and expr.EvalTemplate returns an error for a template that cannot be evaluated instead of writing it into the string. mapping.AssignStruct now enforces validate:"required" on an int field. See the CHANGELOG for the full list of moves.
New public API: expr.Refs returns the keys a value's templates read under a name, such as vars; template errors are *expr.TemplateError, and EvalTemplateMap joins one *expr.FieldError per failed value, naming its path.
Changelog
- 413e63d Build each release platform in a job of its own
- 9e4c45c Bump github.com/jarcoal/httpmock from 1.4.1 to 1.4.2
- 0956d46 Bump github.com/pb33f/libopenapi from 0.37.2 to 0.41.2
- 5465e6c Bump go.yaml.in/yaml/v4 from 4.0.0-rc.4 to 4.0.0-rc.6
- c97892b Check ready as given, report an exit that beats the deadline, and narrow detaching
- ee38c25 Document template errors and show why a job could not start
- f522131 Enforce required on int fields in AssignStruct
- 320efd0 Evaluate vars against a copy and keep orders stable
- 4e4c936 Expect the reason of the first failing iteration in the e2e reports
- ec2fba3 Fail a step whose template cannot be evaluated
- 3c10ebc Find an embedded job file in the graph where the run finds it
- fbf4068 Give every run on main a concurrency group of its own
- 202e933 Hide credentials before an action's log truncates them
- 00327ed Hide every basic_auth credential, also as an action receives it
- 8c435b1 Hide passwords passed to actions wherever Probe shows them
- b5ed053 Indent a failed step's error as its other details are
- 8395526 Keep credentials out of template errors and keep failure kinds in repeats
- acd2a31 Keep the Do tests in client_test.go next to the others
- 09aa0f5...
v1.17.0
External actions
A step can now use an action that lives outside Probe, in a GitHub repository of its own or in a local directory. linyows/probe-graphql is the first one:
- name: Look up Japan
uses: github.com/linyows/probe-graphql@f9e7b841b87dccbcbe71ae10f8562aa637d52a51 # v0.1.0
with:
url: https://countries.trevorblades.com/graphql
query: '{ country(code: "JP") { capital } }'
test: res.data.country.capital == "Tokyo"- A remote action is written as
github.com/<owner>/<repo>[/<dir>]@<commit>and must be pinned to a full 40-character commit SHA. A local one is a path starting with./,../or/, relative to the workflow file. - The action's
action.ymlpoints at its executable with a SHA-256 digest per platform. The commit fixesaction.ymlandaction.ymlfixes the executable, which is checked when it is downloaded and again each time it starts. - Every external action is resolved before the first job, so a bad reference fails the run with exit code 2 and a download does not count against a step's timeout.
See External Actions for how to write and publish one.
For library users
Nothing breaks. The new package actionref resolves a uses to an executable, actionrpc.RunExecutable starts an action from an executable other than probe itself, and RunOptions.BaseDir sets the directory local actions are relative to.
Changelog
- efa0c5b Add an example that queries GraphQL with an external action
- e8d0b85 Cover external actions in the end-to-end tests
- a4cf03c Merge pull request #296 from linyows/feat/external-actions
- 5d13384 Run actions published outside Probe
- 337a1d3 Trust only the action.yml at the pinned commit
- 8f96f25 Write the E2E echo server in Go
v1.16.0
Breaking changes for library users
This release breaks the Go API in a minor version. Helpers that do not depend on the workflow engine moved out of the root probe package into actionrpc, expr, jsonutil, mask, report, safefile, procgroup and truncate, and the DAG renderers into dag. No aliases are left behind; CHANGELOG.md lists every old name and its replacement.
The command line and workflow files are unaffected.
Changes to probe dag --mermaid
- The steps of a job are linked in run order, and drawn top to bottom inside the job.
- Jobs whose IDs differ only in symbols, such as
unit-testandunit.test, are drawn as separate nodes instead of one.
Changelog
- 6f6235e Check gofmt in golangci-lint
- 6c171bf Draw the DAG from a Graph in the dag package
- 65bc014 Give a Mermaid node of its own to a need that names no job
- 12d0f9e Give every Mermaid node its own ID
- 518ecc0 Link a job's steps in run order in the Mermaid DAG
- 2c48bad Merge pull request #288 from linyows/refactor/extract-leaf-packages
- f7bead3 Merge pull request #289 from linyows/refactor/extract-expr
- 9ba84aa Merge pull request #290 from linyows/refactor/extract-actionrpc
- bd43bc6 Merge pull request #291 from linyows/refactor/extract-report
- 9118fbf Merge pull request #292 from linyows/ci/gofmt-check
- 5866e7d Merge pull request #293 from linyows/refactor/dag-renderers
- 61eaf6b Merge pull request #294 from linyows/fix/mermaid-unique-ids
- 34c2c91 Merge pull request #295 from linyows/release/v1.16.0
- 8e7da6e Move helpers without engine dependencies out of the root package
- 1d5282a Move report rendering into the report package
- 5c9b110 Move the action protocol into the actionrpc package
- 8c1f90d Move the expression engine into the expr package
- 5fb4583 Prepare the changelog for v1.16.0
- d21ede5 Run gofmt on the report package
- 6a5d605 Show a custom action that Probe can actually run