Breaking
apifae patch --dry-run now exits 1 when it finds drift
It used to exit 0 for ordinary field drift — the drift patch exists to fix
— and 1 only for drift it could not apply, such as a removed field or a grown
array. That is the inverse of what the manual documented, so a CI gate written
from the documentation got the opposite of what it asked for.
The contract now matches diff, which the manual has always sold it as a pair
with:
| Exit | patch --dry-run |
|---|---|
| 0 | nothing needed changing |
| 1 | drift was found — nothing was written |
| 2 | the run could not complete |
Who this breaks: a pipeline that runs apifae patch --dry-run and treats a
non-zero exit as a failure. It will start failing on workspaces that have drift.
What to do: that is the signal working — a mock has stopped matching the API
and something should change. If you want the older "tell me but do not fail"
behaviour, treat exit 1 as informational explicitly:
apifae patch "$API" --dry-run || [ $? -eq 1 ]Exit 2 still means the run could not complete, and still outranks 1, so this
keeps failing on a connection error.
A real (non-dry-run) patch is unchanged: it exits 0 when it applied
everything it found.
push, pull and login no longer accept flags
--force and --dry-run on push, --force and --version on pull, and
--api-key on login have been removed.
None of them ever did anything. All four commands refuse immediately — APIFae
cloud does not exist yet — so every one of these flags was read by nobody and
silently ignored.
Who this breaks: a script passing one of them. It now fails as an unknown
argument instead of being accepted and ignored.
What to do: drop the flag. The command's behaviour is identical without it.
Their reinstatement, if the cloud is built, is tracked separately.
diff now sees a spec that lives only in schemas/
validate, up, patch and diff looked for the OpenAPI spec under three
fixed names at the workspace root and nowhere else. The schemas: globs in
apifae.yaml had exactly one consumer — status, which counts files — so a
workspace whose spec sat only in schemas/ had no spec at all as far as every
command that reads one was concerned.
apifae init leaves exactly that workspace behind whenever the spec it imported
was not already at the root.
In diff this was not a degradation, it was a silent stop. The spec is the only
input to the contract check, so endpoints_violating stayed at 0 and the run
exited 0 on an API that was breaking its published contract.
Resolution order is now: the three root names first, then the schemas: globs
resolved against the root, then nothing. The root wins deliberately — init
writes the spec to both places, and letting the copy win would mean editing the
spec the manual points at had no effect. Among glob matches, only a file
declaring a top-level openapi: or swagger: key is a candidate, and the first
in sorted order wins, so the choice is identical on CI and on a laptop.
Who this breaks: a pipeline whose workspace keeps its spec in schemas/.
diff will start reporting spec violations it could not previously see, and can
exit 1 where it used to exit 0.
What to do: read the violations — they are contract breaches that were
already happening. Nothing about your workspace needs to change.
diff probes every response variant, not just the first
A mock endpoint can declare several responses, each with its own when: block —
a header, a query parameter, a path parameter. diff built one probe per
endpoint and compared the answer against the first response, so on a versioned
mock it checked one variant and reported the endpoint as covered. Every other
variant was unverified and nothing said so.
It now builds a probe from each response's own when: block and reports each
under its own label:
GET /orders
[header.api-version=2026-01-01]
✓ No drift
[default]
✗ Drift detected:
~ $.total: Number → String
Summary: 1 checked (2 variants), 1 drifted
The label carries the full matcher key, so query.version=2 and
header.version=2 are never confused for one another.
Three kinds of response are reported as skipped rather than guessed at,
because no single request satisfies them: a body.* matcher, any ~ regex
matcher, and a scenario: response. A skip is reduced coverage, not a failure —
it is counted and printed and the run still exits 0. --fail-on-skip now covers
skipped variants as well as skipped endpoints.
An endpoint with a single unconditioned response prints exactly as it did
before. If none of your mocks use when:, nothing about your output changes.
Who this breaks: a pipeline with versioned mocks. diff will start finding
drift in variants it never checked, and can exit 1 where it used to exit 0.
Anyone passing --fail-on-skip may also see it trip on a variant that cannot be
probed.
What to do: the drift was already there. For a variant diff cannot probe,
give it a matcher that can be satisfied — a header or query parameter usually
says the same thing a body field does — or send the value on every probe with
auth.headers.
An apifae.yaml that names a proxy turns off HTTP_PROXY
The proxy settings in apifae.yaml used to do nothing at all, so proxying
happened entirely through HTTP_PROXY, HTTPS_PROXY and NO_PROXY, which the
HTTP client read for itself. Those keys work now — and config replaces the
environment rather than extending it.
The environment is still read when the file says nothing about proxying. The
moment it names any of the three proxy.* keys, the environment stops
applying entirely — so a file setting only proxy.http also turns off
HTTPS_PROXY.
That rule is deliberate. Routing you can read off one file beats routing
assembled from a file and whichever shell happened to launch the process, and
the alternative — merging the two — produces a workspace nobody can reason about
from either source alone.
Who this breaks: a workspace that sets some proxy.* key in apifae.yaml
and relies on the environment for the rest. Before this release the file was
inert, so the environment won by default; now the file wins completely.
What to do: put every proxy setting the workspace needs in one place. If the
environment is where you want them, remove the proxy.* keys from
apifae.yaml — an absent block reads the environment exactly as it always did.
Added
The six outbound config keys are no longer inert
proxy.http, proxy.https, proxy.no_proxy, timeouts.connect,
timeouts.request and timeouts.mock_delay_max were validated, written and
read back by apifae config, and no command consumed any of them. The manual
labelled them "accepted, not yet acted on", which was honest but not much use.
They are live:
diffandpatchreach the API through the configured proxy, and apply
both timeouts when they probe.--timeoutstill beats the file.recordforwards through the proxy and appliestimeouts.connect. It
deliberately ignorestimeouts.request: it is relaying live traffic whose
duration is the upstream's business, and cutting off a slow response
mid-capture would write the fragment into the session as if it were the whole
thing.upclamps a mock's owndelay_mstotimeouts.mock_delay_max. A
response asking for more is served at the ceiling rather than refused —
delay_ms: 300000is almost always a misplaced zero, and turning a typo into
a failed request helps nobody. Offending responses are named once when the
mocks load and again on every reload. The ceiling is fixed at startup, so
changing it under a runningupasks for a restart.
A run that times out exits 2 — could not complete — never 1, so a slow API
never reads as drift.
Two new manual pages
/docs/mock-files documents the mock file format itself — every field, its
type and its default — and /docs/config-file does the same for apifae.yaml.
Both were previously things you learned by reading apifae init's output.
Their tables are checked against the binary, so a field that exists only in the
documentation now fails the build.
/docs/scenarios has become /docs/guides/work-with-scenario-files, a guide
rather than a reference, and the old URL redirects.
Changed
apifae push --help,pull,loginandlogoutnow describe a capability
that does not exist yet, in the conditional, instead of advertising four
working features./docs/referencesays the same.- The manual corrects ten claims about behaviour the binary does not have —
among them whatup --strictactually makes fatal (errors, not warnings),
thatvalidateandpatchdo not need anapifae.yaml, and that
transition_toonly fires on a response taggedscenario: machine:state. apifae up's entry no longer documents an exit code. It serves until
interrupted and has none.--timeoutno longer carries its own default. Left unset it falls through to
timeouts.request, and only then to 5000 ms — which is what it defaulted to
before, so a run that sets neither is unchanged. Previously the flag's default
was applied first and the config key could never have been reached.timeouts.requestis documented as defaulting to 5000 ms, not 30000. The
30000 was the intended default of a key nothing read; 5000 is what the code
has always actually used.
Documentation and site
The manual's transcripts are checked harder than they were: every command and
flag in it now resolves against the running binary, every console block must
declare whether it is executed or exempted-with-a-reason, and the scenario-file
reference — previously outside every guard — is inside them.
The site's footer no longer claims every example is executed, because it was
not. The landing page's three diff transcripts now describe one run rather
than three contradictory ones, and every claim about price has been removed.
The landing page's two lead terminal transcripts — the hero's diff and the
capabilities section's lead transcript — now play as recordings when they
scroll into view. Each recording is generated from the same data array the
static block renders, so the animation and the static text describe the same
run. The static block stays as the fallback: it's what a reader with no
JavaScript, prefers-reduced-motion: reduce, or a screen under 768px sees,
and it's what the prerendered HTML carries. The CSP's script-src gained
'wasm-unsafe-eval', which the player needs to compile its WebAssembly
terminal parser; 'unsafe-eval' remains disallowed.
/docs/reference is structurally sound for the first time: its 87 heading ids
were 32 distinct ones, so every link in its own command overview resolved to an
ambiguous target. Ids are unique now, heading levels nest by command depth
rather than jumping from h2 to h6, and every code block is reachable from
the keyboard.
/docs/scenarios is rewritten. It was the one page still written to a generic
documentation template — Title Case headings, sections that restated their own
heading, three sections written twice — while the rest of the manual is in one
voice. Every example, transcript and YAML block on it is unchanged; only the
prose around them moved. It also now says what DELETE /_apifae/scenarios/active
resets, which the control-API section never mentioned.
Install
Downloads from apifae.com/dl are now counted in aggregate — what was
downloaded and on what day, with no address, cookie or identifier kept. The
CLI itself sends nothing; it gained no telemetry. An install.sh run can
label its channel with APIFAE_INSTALL_SOURCE. /privacy describes exactly
what is recorded.
See https://apifae.com/dl/ or the install instructions at
https://apifae.com.
Downloads and checksums: https://apifae.com/dl/0.2.0/
Verify against dist-manifest.json, not against the URL you fetched from.