fix(deploy): read a quoted key_env, and test the direction that can hurt you - #94
Conversation
preflight extracted a key_env only when the variable name began right after
the whitespace, so `key_env: "UPSTREAM_X_KEY"` and its single-quoted twin were
skipped. Both are legal YAML for a plain string field, the loader reads them,
and it refuses to boot when a named variable is unset — so preflight printed
PASSED and the service then failed to start on the one credential preflight
exists to check. A false PASS in a credential check is worse than the false
FAIL it replaced. One character class closes it.
The guard tests only asserted the harmless direction: a tool that IS on the
service PATH is found, a key_env that IS plain is extracted. A test that never
exercises the failing direction cannot catch a false PASS, which is how this
shipped. Now covered: a tool present on the caller's PATH but absent from the
service's must be reported missing, a quoted key_env must be extracted, and a
`key_env:` inside a trailing comment must not be. Each was confirmed to fail
when its fix is reverted.
Also in the dashboard asset versioning:
- an asset in a subdirectory silently disabled versioning for EVERYTHING,
because fs.Glob returns the directory as a name and reading it fails.
WalkDir instead, so nested assets are versioned rather than sinking the
whole mechanism back to the stale-asset bug it was written to fix.
- the hash's documented "a rename also moves the token" property is tested.
- drop four lines that set a Content-Type http.ServeContent already sets,
and assert the served type instead.
- name the ceiling on the blind reference rewrite: a quoted asset name that
is not a URL is rewritten too.
Signed-off-by: Osher-Elhadad <Osher.Elhadad@ibm.com>
OsherElhadad
left a comment
There was a problem hiding this comment.
Review — 03f1b5e
APPROVE-WITH-NITS. Both halves of the claim hold. The reported false PASS is closed,
and — the part that matters more — the guard test now fails in the direction that can hurt
you: the append-instead-of-override mutation that stayed green against #92 is now
red. The WalkDir swap is correct and measurably behaviour-preserving. The ponytail:
no-collision claim is checkable, and it is true.
One item I would want landed (R1): the false-PASS class is not closed, only the reported
instance — and the new comment tells the next reader the function fails safe, which is
true for the case it names and false for three others. Remedy is two lines plus two fixture
lines, supplied below. Everything else is a nit.
No dollar, token or latency figure is claimed, so there is nothing to re-derive — the right
choice for this change, and worth saying out loud.
How F1 was checked
A regex over config is only right or wrong relative to what the parser does, so rather
than eyeball shapes I built an oracle: a program linking the real config.LoadUpstreams
that prints the key_env names it would demand, diffed against the shipped extraction over
the same file. 41 inputs. A name in truth but not in sed is a false PASS; the reverse is
merely annoying.
The four shapes in the PR body are correct, and so are eleven nobody had tried: tab after
the colon, key_env: with no value, key_env: "", a digit-leading name, an
underscore-leading name, CRLF, a YAML anchor and its alias, !!str, a ---
multi-document header, a 65 KB line, no trailing newline, and a single flow-mapped entry.
Four inputs the loader itself rejects produce noise, which cannot matter — the service will
not boot on them either.
Five diverge.
R1 — the false-PASS class is not closed, and the new comment says it is
deploy/service/install.sh:70-82
The fix makes the extraction independent of quoting. It does not make it independent of
line structure. Measured, loader-vs-preflight:
| input | loader demands | preflight extracts |
|---|---|---|
upstreams: [{…, key_env: UPSTREAM_A_KEY}, {…, key_env: UPSTREAM_B_KEY}, {…, key_env: UPSTREAM_C_KEY}] |
A, B, C | C only |
upstreams: [{name: 'a#b', …, key_env: UPSTREAM_A_KEY}] |
A | nothing |
"key_env": UPSTREAM_A_KEY |
A | nothing |
key_env: >-⏎ UPSTREAM_A_KEY |
A | nothing |
Three root causes, none of them the quoting this PR fixed:
- One name per line.
s///phas nogand the leading.*is greedy, so on a line
with severalkey_env:only the last is taken. s/#.*//is not YAML-aware. A#inside a quoted scalar earlier on the line is not
a comment, but the substitution treats it as one and erases the realkey_env:after it.- The colon must touch
key_env."key_env":— a quoted mapping key — never matches.
Each ends the way the PR body describes: Preflight PASSED, then LoadUpstreams refuses to
boot, and Restart=always makes it the crash loop install.sh's own header (lines 17-20)
exists to prevent.
The fail-safe claim about a lowercase name is true, and I checked it end to end rather
than by reading: key_env: upstream_a_key extracts _, which the credential mapping
(install.sh:334-341) turns into $CREDS/- and $CREDS/_, neither of which exists, so
preflight prints ✗ and returns 1. Same for the U and KEY that mixed-case and flow-style
noise produce. That direction is safe as advertised.
Exposure today is nil, which is why this is a nit and not a block: the shipped example is
block style, the deployed allow-list is block style, and no doc invites flow style. Note too
that the plausible inline style — one flow-mapped entry per line, - {name: a, …, key_env: UPSTREAM_A_KEY} — extracts correctly, because there is still one key_env per line. It
takes two entries on one physical line to break it.
But the body says "No special case per quoting style, and nothing to update when a third one
appears," and install.sh:76-79 tells the next reader that being wrong here lands in the
safe direction. For the four rows above it lands in the unsafe one, and a wrong safety claim
in a comment is what stops the next person from checking.
Fix — 2 lines, differentially tested over all 41 inputs, closes all four rows, regresses
nothing (the shipped example and the deployed allow-list both still yield nothing at rc 0):
configured_key_envs() {
sed -E 's/(^|[[:space:]])#.*//' "$1" 2>/dev/null | tr ',{}' '\n\n\n' \
| sed -nE 's/.*key_env[^:A-Z0-9_]*:[^A-Z0-9_]*([A-Z0-9_]+).*/\1/p' | sort -u
}(^|[[:space:]])#is what YAML actually calls a comment, so'a#b'survives while a real
trailing comment still dies. Stripping before thetrkeeps a comma inside a comment
from resurrecting the tail of it.tr ',{}'puts each flow-mapped entry on its own line, so "only the last per line" stops
mattering.key_env[^:A-Z0-9_]*:accepts a quoted key. Still no quote character in the sed
script, so the'\''escaping you avoided stays avoided.
Two fixture lines guard it, in the same test:
- {name: flow, dialect: openai, base_url: 'https://x#y', key_env: UPSTREAM_FLOW_A_KEY, header: X-Api-Key}
"key_env": UPSTREAM_QUOTED_NAME_KEYBlock scalars stay broken and are worth leaving broken — nobody writes a folded scalar for an
env var name, and closing it needs a real parser. Say so in the comment instead of claiming
generality. If you would rather not touch the code, narrowing those two claims is enough to
make this a documentation nit.
R2 — nit: the body overclaims what caller-tool-is-runnable-here=yes proves
The body says that without it, caller-only=missing "could pass because the tool was simply
broken rather than because the resolution is correct." Broken is the one case this assertion
cannot see — bash's command -v, the primitive in_service_path itself uses, reports a
non-executable file as found:
$ chmod 600 $d/probe600; PATH="$d:$PATH" bash -c 'command -v probe600 && echo FOUND'
/…/probe600
FOUND # unprivileged, mode 0600
and planting the caller-only tool at 0o600 instead of 0o700 accordingly leaves the test
green.
What it does guard is a vacuous pass, and it guards that properly: typo the planted
filename and the test goes red; remove the assertion and typo the plant and it goes green.
The line is load-bearing. The in-code comment at preflight_test.go:143-144 already says
this correctly ("a typo in the tool name") — only the body's wording drifts.
R3 — nit: the new hash test proves the NAME is in the hash, not the LENGTH
dash/assetversion_test.go:135-152, dash/ui.go:93-96
| mutation | test |
|---|---|
drop %s\x00%d\x00 entirely |
red |
| keep the length, drop the name | red |
| keep the name, drop the length | green |
keep both, drop the \x00 delimiters |
green |
Both new sub-cases are decided by the NUL-delimited name alone — in {a.js:"xy", b.js:""}
vs {a.js:"x", b.js:"y"} the delimiter already lands in a different place. So "the name
and the length … go into the hash" is guarded at half strength.
Keep the length — it is what makes the concatenation injective, and it is reachable to
defeat without it: {"a": "b.js\x00xy"} and {"a": "", "b.js": "xy"} hash identically once
%d is gone. One clause in the test's comment, or a third pair differing only in a length.
R4 — nit: the walk closure aborts the walk, so one bad entry still means "no assets"
dash/ui.go:73-82
Reading it closely, as it deserves: it is correct as written, on three counts. The
err != nil check precedes d.IsDir(), so the nil DirEntry that comes with a walk error is
never dereferenced. It is unreachable in production — fsys is fs.Sub(embed.FS), every
byte is in the binary, and ReadDir on an existing embedded directory has nothing to fail at.
And aborting is the better choice anyway: swallowing the error would hash a partial asset
set and mint a token that does not describe what is served, where falling back to unversioned
is the fail-open the doc comment promises. Mutating the closure to return nil leaves both
dash tests green, so the path is untested — the right amount of testing for a branch that
cannot fire.
R5 — nit: the new comment's illustrative PATH example does not hold on a usrmerge distro
cmd/context-guru-proxy/preflight_test.go:123-126
"sudo's secure_path carries /sbin and /bin, systemd's default PATH does not." Literally
true, but /sbin and /bin are symlinks into /usr on any usrmerge distro, and systemd's
default PATH contains /usr/sbin and /usr/bin — so a tool "in /sbin" is reachable by the
unit after all, and that example produces no discrepancy. The real one is the reverse,
already documented at install.sh:56-63: /usr/local/bin is in systemd's PATH and not in
sudo's secure_path. The test's mechanism is right — it plants a directory on the caller's
PATH and not the service's — only the prose picked the wrong illustration.
The blind-rewrite ceiling
The no-collision claim is true, by two independent measurements. Enumerating every quoted
occurrence of every asset name across all five assets, with the same loose bracketing the code
uses (["']name["'], mismatched quotes included), yields exactly four hits — and instrumenting
the real versionedUI counts exactly four rewrites:
index.html: 3 rewritten reference(s)
tools.js: 1 rewritten reference(s)
TOTAL = 4
All four are real references: index.html:7 href="style.css", :742 src="app.js", :745
src="tools.js", tools.js:35 href: 'tools.css'.
Recommendation: do not land the attribute match — and change the recorded upgrade path,
because as written it is a regression. The fourth reference is dash/ui/tools.js:35:
document.head.appendChild(el('link', { rel: 'stylesheet', href: 'tools.css' }));A JavaScript object literal, not an HTML attribute. "Match on the enclosing attribute" would
stop matching it, tools.css would go back to an unversioned URL at max-age=3600, and that
is exactly the stale-stylesheet bug the one-token-for-all-assets design exists to prevent —
named in this same file at ui.go:43-46. The blindness is load-bearing because references
live in both an attribute and an object literal. Tightening the match is not a strictly-safer
upgrade; it trades a hypothetical silent over-rewrite for a concrete silent under-rewrite of a
reference that exists today.
What should land instead, if anything: make the failure loud rather than the match
narrower. The worry is sound — a silent miss in the response path of every page load is the
shape this whole thread started with — but the cheap fix is a test, not a regex. The rewrite
count is knowable and stable at 4, and the test already enumerates the references it expects.
Three lines asserting that the total number of ?v=+assetVersion occurrences across
versionedUI equals that enumeration turns any future collision into a red test instead of a
silent rewrite, at zero runtime cost. So: keep the blind rewrite, keep the ceiling named
(naming it is sufficient for the code), replace "Match on the enclosing attribute if one
ever appears" with the reason it is not an option, and optionally add the count assertion —
which is the guard the ceiling actually wants.
Do the new tests guard their properties?
Mutation is the only thing that establishes that a guard test guards anything. Every fix was
mutated and the tests re-run, -count=1 throughout.
| mutation | test |
|---|---|
revert to [[:space:]]* (the #92 defect) |
red |
| strip whole-comment-lines only (F4's gap) | red — F4 closed |
| no comment stripping at all | red |
over-strip: /#/d |
red |
| accept lowercase names too | green — not a claimed property; uppercase-only is documented |
| append the service PATH instead of overriding | red — was green against #92 |
bare command -v (pre-#92) |
red |
| hardcode systemd's default PATH | red |
| typo the planted caller-only tool | red (vacuous-pass guard works) |
plant the caller-only tool 0o600 |
green — R2 |
drop callerBin from the test PATH |
red |
revert fs.WalkDir to fs.Glob(fsys, "*") |
red — F8 closed |
WalkDir but SkipDir on every subdirectory |
red |
| walk closure swallows the error | green — R4 |
| drop name+length from the hash | red |
| drop the name, keep the length | red |
| drop the length, keep the name | green — R3 |
drop the \x00 delimiters |
green — R3 |
ServeContent(w, r, "", …) |
red — F6's new assertion guards |
rewritable = map[string]bool{} |
red |
Both headline gaps from the #92 review are shut, and shut in the direction that matters.
F4–F7 and P2
- F4 — closed. The
# key_env: UPSTREAM_NEVER_CONFIGUREDfixture plus the exact compare
makes whole-comment-lines-only stripping red. - F5 — taken, half-guarded. See R3; keep the length.
- F6 — taken, and beyond the ask. The dead block and the
mimeimport are gone and a
Content-Type assertion was added.pathis still needed forpath.Ext; vet and build clean. - F7 — taken. The claim inside the comment verifies; the upgrade path inside it does not.
- P2 — rejected, and I agree.
in_service_pathexists to resolve a tool the way the
unit will; a tool the caller can see and the unit cannot must read as missing, or you get
the same false PASS in a different check. Appending reinstates exactly that, and the new
caller-only=missingassertion now locks the override in — I re-ran the append mutation and
it is red. The source is right too: no unit setsEnvironment=PATH=, so
systemctl show-environmentis the manager environment the units inherit rather than an
approximation, and the${p:-$PATH}fallback preserves the old behaviour wheresystemctl
is absent. Only quibble: the rejection is recorded implicitly, as a test, not in the body —
and a test is the better record, so I would not change it.
Verified sound
- F8 is behaviour-preserving.
assetVersionis unchanged froma33c9ac, so swapping
GlobforWalkDirover today's flat asset directory changes no URL and busts no cache. The
new subdirectory case versions correctly:sub/extra.jsis hashed at its slash-separated
name and rewritten to"sub/extra.js?v=…", which is also the name the handler looks up, so a
nested asset is served fromversionedUIrather than falling through.WalkDirskips the
.root, so no directory entersnames; lookup is an exact map hit, so nested keys add no
traversal surface. Untouched edge, not this PR's: a nestedsub/index.htmlwould get
max-age=3600rather thanno-cache, sinceswitch namecompares the whole path. - No regression on the real inputs. The shipped example and the deployed allow-list both
extract nothing at rc 0, unchanged. This also re-confirms #92's F3: the deployed allow-list
configures nokey_env, soPreflight PASSEDthere exercises the zero-configured path — the
right evidence for the reported bug, and not evidence for the quoted case, which the unit
test now covers instead. - Conventions. DCO sign-off present; author and committer are both the repo identity;
conventional-commitfix(deploy):title written as a human would. No AI attribution
anywhere in the diff, commit message or body. No secret, key, token, UUID, username or home
path in the added lines.schemaVersionstill6atdash/schema.go:43, untouched. - Scope is clean. Four files, every hunk in service of something the body claims, no
unrelated refactor, nothing left dead.
Commands
gofmt -l . # empty
go vet ./... # exit 0
CGO_ENABLED=1 go test -race -count=1 ./... # 25 packages ok, exit 0
# both #91 flakes green in this runPlus the differential oracle (a main package with a replace onto the branch, linking
config.LoadUpstreams) over 41 inputs; a credential-mapping replay of install.sh:334-341
against the extracted noise; the mutation matrix above, each applied to a throwaway copy and
reverted; a quoted-occurrence audit of dash/ui/* plus a throwaway dash test counting ?v=
in the real versionedUI. Nothing was pushed to the branch and no service was touched.
The previous fix made the extraction independent of quoting but not of line
structure, so three legal shapes still let the loader demand a credential
preflight never checked — the same false PASS, in a different shape:
- two or more key_env on one physical line (flow style): the substitution
has no /g and a greedy leading .*, so only the LAST was taken. Three
entries yielded one.
- a `#` inside a quoted scalar earlier on the line (a base_url with a
fragment): s/#.*// is not YAML-aware and erased the real key_env after it.
One entry yielded none.
- `"key_env": X`, a quoted mapping key: the colon had to touch key_env.
Each ends the way the first one did: Preflight PASSED, then LoadUpstreams
refuses to boot, then Restart=always turns it into the crash loop this script
exists to prevent.
Checked differentially rather than by eye: an oracle links LoadUpstreams and
prints the key_env names it would demand, and the extraction is diffed against
it over 41 inputs covering every shape the loader accepts. All three shapes now
match; the shipped example file and a real allow-list still yield nothing at
rc 0. A block scalar (key_env: >- with the name on the next line) stays
unhandled, and the comment says so instead of claiming generality — closing it
needs a parser, which is the recorded upgrade path in preference to a fifth sed
stage.
The comment also no longer tells the reader the function fails safe: it names
which shapes miss loudly and which one misses in silence.
Comment stripping has to happen BEFORE the line is split, or the split hands
the tail of a comma-bearing comment back as configuration. The fixture's inline
comment gained a comma so that ordering is a test rather than a claim.
Signed-off-by: Osher-Elhadad <Osher.Elhadad@ibm.com>
The recorded upgrade path for the blind asset-reference rewrite was a
regression. "Match on the enclosing attribute if one ever appears" would drop
one of the four real references: tools.js builds its stylesheet link in a
JavaScript object literal, el('link', {href: 'tools.css'}), not an HTML
attribute. Narrowing the match would silently stop versioning tools.css and
hand it back an unversioned URL at max-age=3600 — the stale-stylesheet bug the
one-token-for-all-assets design exists to prevent, named at the top of the same
file. The blindness is load-bearing because references live in both forms, so
the comment now argues against narrowing and points at the loud alternative.
That alternative lands here: total the rewrites actually performed and compare
them with the references the test already enumerates. A quoted asset name that
is not a reference now turns this assertion red instead of being versioned in
silence — verified by planting one, which trips this assertion and nothing else,
and stays silent once the assertion is removed.
Two comments claimed more than the code delivers:
- the asset hash. Both of the new sub-cases are decided by the NUL-delimited
NAME alone, so neither exercises the %d length. The length still belongs
there — it is what makes the concatenation injective, and without it
{"a": "b.js\0xy"} and {"a": "", "b.js": "xy"} hash identically — so that
reason is now recorded where the length is written, rather than implied to
be under test.
- the PATH test's illustration. /sbin and /bin being absent from systemd's
default PATH produces no discrepancy on a usrmerge distribution, where both
are symlinks into /usr. Replaced with the real case install.sh already
documents: /usr/local/bin is in systemd's PATH and not in sudo's
secure_path. The mechanism was right; only the example was wrong.
Signed-off-by: Osher-Elhadad <Osher.Elhadad@ibm.com>
Follow-up to #92, which was merged before its review landed. The review found that one of
its two fixes introduced a false PASS — strictly worse than the false FAIL it replaced.
The review of this PR then found that the same false-PASS class was still open in three
more shapes, so the extraction below is the second iteration and this time it was checked
against the parser rather than against a fixture.
The defect
#92 rewrote the credential extraction as:
[[:space:]]*requires the variable name to begin immediately after the whitespace, so aquoted value is not extracted at all:
Both forms are legal —
config/upstreams.go:30is a plainKeyEnv stringand the loaderparses them — and
config/upstreams.go:93refuses to boot when a named variable isunset. So the sequence was: preflight prints
Preflight PASSED, the operator proceeds,and the service then fails to start on precisely the credential preflight existed to check.
Under
Restart=alwaysthat is a crash loop.A false PASS in a credential check is worse than the false FAIL it replaced.
The fix
A regex over config is only right or wrong relative to what the parser does, so the
check is a differential one: an oracle links
config.LoadUpstreamsand prints thekey_envnames it would demand, and the extraction is diffed against it over 41 inputscovering every shape the loader accepts. A name in truth but not in the extraction is a
false PASS; the reverse is only an annoying false FAIL.
Three independent causes had to be closed, none of them the quoting the first attempt
addressed:
key_env: "X"/key_env: 'X'key_envon one physical line (flow style)#inside a quoted scalar earlier on the linekey_env"key_env": X— a quoted mapping keykey_env: >-with the name on the next line (block scalar)So: comments are stripped the way YAML defines them (
(^|[[:space:]])#) rather than at any#; the line is then split on the flow separator so "one name per line" stops mattering;and the colon no longer has to touch
key_env. The order is load-bearing in bothdirections and a test pins it: strip after splitting and the tail of a comma-bearing
comment comes back as configuration.
A block scalar stays unhandled and the in-code comment now says so rather than claiming
generality. Nobody folds an environment variable name, and closing it needs a real YAML
parser — which is the recorded upgrade path, explicitly in preference to a fifth
sedstage. The two other unhandled shapes (a lowercase or mixed-case name) at least fail
loud: they extract noise that matches no credential file and print
✗.Every stage was mutated and the test re-run: reverting to the previous extraction, dropping
the split, un-YAML-ing the comment strip, requiring the colon to touch
key_env, strippingcomments after the split, stripping none, and over-stripping (
/#/d) each turn a testred.
The test gap that let it ship
TestPreflightResolvesToolsOnTheServicePATHasserted only that a present tool isfound — the direction that cannot hurt you. A test that never exercises the failing
direction cannot catch a false PASS, which is exactly how this shipped.
It now asserts all four states:
What the first assertion guards is a vacuous pass, not a broken tool. Typo the planted
filename and the test goes red; remove the first assertion and typo the plant and it goes
green — so the line is load-bearing. It cannot detect a tool that is present but
unrunnable, because
command -v— the primitivein_service_pathuses — reports amode-
0600file as found (measured unprivileged; the same file cannot be executed).That case is outside what this assertion can see, and the in-code comment says so.
Also closed, from the same review
A
ui/subdirectory silently disabled ALL asset versioning.fs.Glob(fsys, "*")returns a subdirectory as a name,
fs.ReadFileon it errors, and the whole function thenbails to "no assets" — so one nested file would turn versioning off everywhere and bring
back the stale-asset bug #92 existed to remove, silently and one level down. Replaced with
fs.WalkDir, which covers assets in subdirectories at their slash-separated names — whichis also what a reference to one looks like in the markup.
The remaining ceiling on the rewrite is named in a
ponytail:comment rather than paperedover: it matches quoted asset names blindly, so a quoted occurrence that is not a URL (a JS
string literal, a CSP nonce, inside a percent-encoded data URI) would be rewritten too.
There is no collision across the five assets today — four rewrites, all real references.
The upgrade path is NOT to narrow the match, and the comment now says why. Matching on
an enclosing HTML attribute would be a regression: one of the four references is a
JavaScript object literal,
el('link', {href: 'tools.css'})indash/ui/tools.js, not anattribute. An attribute match would stop versioning
tools.css, hand it back anunversioned URL at
max-age=3600, and reintroduce the stale-stylesheet bug theone-token-for-all-assets design exists to prevent. The blindness is load-bearing precisely
because references live in both an attribute and an object literal.
What lands instead makes the failure loud rather than the match narrower: three lines
totalling the rewrites actually performed and comparing them against the references the
test already enumerates. Planting a quoted asset name that is not a reference now turns
that assertion — and only that assertion — red; remove the assertion and the same plant is
silent again.
Two comment corrections in the same area, both cases of a comment claiming more than the
code or the test delivers:
decided by the NUL-delimited name alone, so the comment no longer implies they cover the
length. The length stays, with its real reason recorded where it is written: it is what
makes the concatenation injective, and without it
{"a": "b.js\0xy"}and{"a": "", "b.js": "xy"}hash identically./sbinand/binbeing absent from systemd's defaultPATH. On a usrmerge distribution those are symlinks into
/usr, which systemd's PATHdoes contain, so the example produced no discrepancy. Replaced with the real one already
documented in
install.sh:/usr/local/binis in systemd's PATH and not in sudo'ssecure_path. The test's mechanism was right; only the prose was wrong.Verification — both directions, on the real host
sudo ./deploy/service/install.sh preflight→Preflight PASSEDagainst the deployedallow-list, unchanged from before this PR, and the extraction still yields nothing at rc 0
on the shipped
upstreams.example.yaml— the no-regression case, since the shipped file'sdocumenting comment is the false FAIL #92 set out to fix.
The other direction, against a temporary config that configures a quoted
key_envand aflow-mapped one on a single line, with no credential files present: both names are demanded
and preflight prints
Preflight FAILEDwith a✗for each. Nothing live was modified,restarted or reconfigured.
CGO_ENABLED=1 go test -race -count=1 ./...→ 25 packages ok, exit 0.go vetclean.gofmt -l .empty. No schema change;schemaVersionuntouched at 6.