Troubleshooting¶
The errors you’re most likely to hit, and what each one means.
Getting started¶
not inside a seal project¶
seal walks up from your current directory looking for one with
both a Tiltfile and a services/ subdirectory. Run it from inside
such a directory – for the bundled example, examples/angular-django/.
not inside a service directory¶
seal run only works from inside services/<name>/, because that’s
how it knows which service’s .env you mean. From the project root, you
want seal up instead.
Tiltfile not found (from tilt, not seal)¶
Your project has no root Tiltfile. See Declaring your
services.
Credentials¶
A container starts without its credentials¶
Symptoms: a readiness probe that never passes, or an application error about a setting it was configured to require. The object the container reads was deployed empty.
You ran tilt up or tilt ci directly. Seal fills the objects your
overlay annotates just before Tilt starts, and a bare tilt run does not
refuse – it deploys the annotation without the values, so the failure
surfaces well away from its cause. Run seal up/seal ci
instead.
The same symptom after editing a .env: re-run seal up (Ctrl-C
the running session first) to refill.
A key reaches no container¶
seal check names it:
overlay '<name>': Deployment '<name>', container '<name>' reads <service>'s
credentials but has no source for '<KEY>'.
Either that container’s envFrom does not list the object claiming the key,
or no object in that overlay claims it. An overlay that splits a service
across several objects has to repoint every container that reads it, not
just one.
'<cli>' not found on PATH¶
Install the CLI your seal-credentials-config.json names. The message
carries that provider’s own install_hint, so it tells you where to get it.
It’s only needed if a .env you’re using actually references that
provider’s scheme – a project whose values are all literals and k8s://
markers never reaches for a store at all, and needs no CLI installed.
In CI, this means your provider_setup step didn’t run or failed.
A provider can’t resolve an item, or isn’t logged in (in CI)¶
Check four things:
The run selected the credentials environment you think –
--credentials_env, elseSEAL_CREDENTIALS_ENV, else the config’sdefault_env. It is independent of--k8s_overlay.seal-credentials-config.jsonmaps that provider for that environment.seal checksays so by name if it does not.Your provider’s token is set on the GitHub Environment this job bound to, not as a repository secret, and the caller passes it as the reusable workflow’s
provider_token.Your caller workflow names that secret in its own
secrets:block – without the passthrough it silently arrives empty. See the three quirks.
There is deliberately no fallback here: this same .env is what production
resolves too.
Bringing an environment up¶
A service you just added doesn’t show up in Tilt¶
Check your root Tiltfile actually include()s it. Tilt re-evaluates the
whole Tiltfile from scratch on every run, so there’s nothing stale to clear.
seal ci refuses to start, naming Deployments¶
Every Deployment container needs a readinessProbe, or “every resource is
ready” means less than it says. Add one per container – an HTTP endpoint
where there’s a server, a command where there isn’t. See
Readiness.
Every offender is reported at once, so you can fix them in one pass.
no JUnit report was written to ..., or the JUnit report holds no test cases¶
From seal_tests_verdict_<service>: that service’s results came
back, but nothing in them says its tests ran. Seal reads the JUnit
report and nothing else, so this is a failure rather than a pass.
Check, in this order:
Your
test_commandactually writes a JUnit report (pytest --junit-xml=..., karma-junit-reporter,--reporter junit).It writes it inside the
junit_tests_directoryyou declared.The suite collected something. A runner that found no tests writes a valid, empty report, and an empty suite is not a green one.
seal_tests_run_<service> in the Tilt UI shows what the command
actually did, and tests-results/<service>/ holds whatever came back.
no results came back from the test container¶
The syncback found nothing to copy: the container never started, or was killed before its results were written. Look at that service’s own resource in the Tilt UI first – this resource is downstream of it.
seal ci fails but seal up works¶
That’s the design: seal up runs none of the checks, so a
half-finished manifest doesn’t stand between you and your cluster. Run
seal check to see what seal ci would refuse.
Outcome tests¶
An outcome is reported no verdict¶
The test ran, or was meant to, and left nothing to read at
/outcome-results/<epic>/<outcome>/passed. This counts as a failure, not
a gap – every way a test silently fails to run looks like an absent file
from here.
Usual causes: the runner exited before reaching that outcome (a container
that exits non-zero takes its resource down with it), or a custom runner
isn’t writing the verdict file. Under a tap group it means no test point
named that outcome – because the run stopped before reaching it, or because
the point carried a # SKIP or # TODO, neither of which establishes
anything. Check
tests-results/outcomes/<group>/<epic>/<outcome>/ and the runner’s logs,
and re-read the contract its runner meets –
a container’s,
or a tap group’s.
A tap group’s run won’t start¶
seal check refuses a tap group with no run at its root, and one
whose run isn’t executable. The second is the one people hit: git records
the executable bit, so a file that is plainly there can still be unable to
start.
chmod +x outcomes/<group>/run
git update-index --chmod=+x outcomes/<group>/run
Nothing could start the group otherwise, and every promise under it would be reported untested for a reason no listing explains.
A tap runner’s points don’t match the outcomes it was given¶
Seal matches each test point’s description against the slugs it passed
after --, spelled from inside the group (<epic>/<outcome>, not
<group>/<epic>/<outcome>). A point naming anything else is reported against
the run and files no verdict, and a promise no point named keeps no verdict.
Echo the slug you were handed rather than composing one: ok 1 - $slug.
An outcome is reported no test yet but there’s a test right there¶
The group’s runner can’t start what’s in the directory. Under playwright
that means no file it collects – *.spec.ts, *.test.ts, or the .js
/.tsx spellings – at any depth under the outcome.
seal check refuses exactly this case, because the suite on its own
would report the promise as untested while the test sits in the tree.
seal check refuses something under helpers/¶
A group’s helpers/ holds what its tests share, so nothing walks it for
outcomes and a playwright run never looks inside it. Two things filed
there would therefore go unnoticed, and both are refused instead: a
prompt.md, which is a promise no run would start (helpers is not a name
an epic can have), and a spec file, which is a test nothing collects. Move
the first under an epic of its own; move the second into the outcome it
tests, or rename it for what it is if the tests import it.
A helpers/ at the tree’s root is refused for a different reason: a runner
is built from one group’s directory, so nothing above it is in the image that
has to import it. Keep one per group, or point a custom runner’s
dockerfile_context at a directory holding both groups. See
Helpers.
seal check complains about a group or runner¶
The two ways the tree and seal-test-config.json drift apart: a group
directory no runner names, or a runner naming a group that isn’t there.
That’s the one thing about the tree a walk can’t answer, so it’s the one
thing that has to be declared – and therefore the one thing that can go
stale. See
seal-test-config.json.
seal check refuses waitForTimeout¶
A sleep in an outcome test – or in a helper it imports, where every outcome
importing it inherits the wait – holds on the machine it was written on and
fails wherever the browser is slower. Wait on the state instead –
expect(locator).toBeVisible() or locator.waitFor() retries until it
holds, and gives up only when it never does. See Wait on the
state.
seal outcomes run <slug> says the session isn’t up¶
Naming outcomes runs them against a session seal up already has
running. Start one first.
A test passes alone and fails in the suite¶
Outcomes run one at a time against the same application, each starting from whatever the one before it left. A full run resets each service that declared one before the run, not between outcomes – so write the test to own the data it asserts on. See Deterministic test state.
seal outcomes run --reset --all --confirm is what finds this without
guessing: it re-runs each failure on its own, from a reset, and reports
FAIL (not confirmed) where the promise then held.
An outcome’s Kubernetes name is too long¶
A generated name is seal-outcome-<epic>-<outcome>, and Kubernetes
allows 63 characters. Seal fails loudly rather than shortening it: two
outcomes silently sharing one Deployment is worse than being told to rename
a directory.
Getting back to green¶
--reset needs outcomes to run¶
seal outcomes run with nothing named reads the verdicts a run
already left behind and starts nothing – that’s the form seal ci
gates on, and there’s nothing there for a reset or a confirmation to come
before. Add --all to run every promise now, against a session seal up already has up. The same message appears for --confirm.
--all runs every promise the tree holds, so there is nothing for a slug to add¶
--all and naming outcomes are different asks, and there’s no reading of the
pair that isn’t a guess – one of the two would report a subset under a flag
that says otherwise. Pick one.
An outcome is reported FAIL (not confirmed)¶
It failed in the run and then held on its own, from a reset. Nothing you change in the application will settle it. It’s one of two things:
the test doesn’t own the data it asserts on – it passed behind a different outcome than it expected;
the test isn’t deterministic – most often it asserts before something has rendered.
Both are the translation’s to fix, so both need a code owner. It still fails the run, and is never reported as a pass: the suite didn’t see that promise kept. See Getting back to green.
seal check refuses a quarantine with no reason¶
A quarantine.md says a failure doesn’t fail the run. One with nothing
written in it is indistinguishable from a test somebody switched off, and the
code owner approving the diff is who has to tell those apart. Say what is
being fixed and what would let the promise come back to the gate.
seal outcomes touched names paths and exits non-zero¶
That’s it telling you the change reaches the outcome tree, which means it can’t merge unattended any more – a code owner has to approve every path it named. That isn’t a mistake in itself: a promise has to be able to change, and a translation has to be written. It is a mistake if what you meant to write was an application fix, in which case the fix isn’t there yet.
nothing to compare against from seal outcomes touched¶
It measures a change from where your branch left the repository’s default
branch, and it looks for origin/HEAD, origin/main, main and master
in that order. A repository with none of those – a fresh checkout with no
commits, or a differently-named trunk – needs the ref naming: seal outcomes touched --since <ref>.
The merge gate¶
seal check says an outcome isn’t covered by CODEOWNERS¶
Either no rule matches it, or the last one that does names no owner.
Remember that the last matching rule wins, not the most specific, and
that a rule with no owners clears ownership. A bare *.py line anywhere
below your tree’s rule silently unlocks every Python test in it.
Cover the directory, not the files: that’s what covers files added to it later.
seal check passes but changes still merge without review¶
A green seal check says the tree is owned, not that a merge is
gated. CODEOWNERS on its own assigns reviewers and lets a pull request
merge unreviewed. Branch protection is the other half, and only a repository
administrator can apply it. See The half Seal can’t
check.