Skip to content

fix(service): validate --upload before spending anything - #482

Merged
scttfrdmn merged 1 commit into
mainfrom
fix/service-upload-validation
Aug 3, 2026
Merged

fix(service): validate --upload before spending anything#482
scttfrdmn merged 1 commit into
mainfrom
fix/service-upload-validation

Conversation

@scttfrdmn

Copy link
Copy Markdown
Contributor

Found while validating #409 against real EC2 — I made this mistake myself, and the dry run rendered a plan that couldn't work.

The bug

--upload takes a value, so this:

spawn service ./svc --instance-type t4g.micro --ttl 20m --upload --region us-east-1

binds --region as the filename and leaves us-east-1 to be parsed as a positional argument. The service gets a stray argv element, the region silently goes unset, and the dry run happily prints:

Instance:    t4g.micro in -
Command:     './svc' 'us-east-1' --addr 127.0.0.1:0
Upload:      --region → /tmp/spawn-service-bin

No cost line, because there's no region to price — but nothing says so.

Three changes, ordered by what they cost

Validation moves before the launch. uploadToInstance already stat'd the file, but only after an instance was running — so a typo cost a launch. It's now checked during argument parsing, before the AWS client is even built. This is the substance of the fix; the message improvements below are the cosmetics.

A flag-shaped value is named as such:

--upload got "--region", which looks like a flag, not a file: --upload takes the
local file to copy, so it swallowed the next argument. Write --upload <file>
(or --upload=<file>) and put other flags after it

rather than --upload --region: stat --region: no such file or directory, which is technically true and buries the lede. Directories are rejected too.

The dry run explains a missing cost bound instead of dropping the line. Silence reads as "this is free", and an unset region is the symptom of this exact mistake, so naming it points at the cause:

Rate:        unknown — no region resolved yet (pass --region to see the cost bound)

Validation runs before the --dry-run branch, not just before the launch: a preview that renders an unusable plan is worse than no preview, since catching this for free is the whole point of --dry-run.

Also: a missed reference fragment

This commits docs-gen/service.md, which #409 generated but never committed. The drift gate is git diff --exit-code docs-gen/, which sees tracked files only — so a brand-new untracked fragment passes CI and would first fail at tag time, where make check-docs runs as a release guard. Worth knowing the gate has that blind spot.

Verification

All three guards mutation-tested:

Mutation Result
if strings.HasPrefix(path, "-")if false TestValidateServiceUpload fails
validation removed from RunE TestServiceRejectsABadUploadBeforeSpendingAnything fails
missing-region note deleted TestRenderServiceDryRunSaysWhyTheCostBoundIsMissing fails

The second mutation exposed a flaw in my own test worth calling out: it initially reached a real AWS account (30s, a live DescribeInstances) because this shell has AWS_PROFILE set. It never launched anything — the instance ID was nonexistent, and I leak-checked three regions to confirm zero instances — but a unit test whose claim is "this never reaches AWS" must not depend on the ambient environment being empty. It now neutralises the credential env vars and points the config/credential files at an empty temp dir, so the same mutation fails in 0.00s having contacted nothing.

Also added spawnRegion to resetServiceFlags, since it's the root persistent flag shared with every other command and these tests now set it.

make check-docs passes. Full cmd suite green except TestCatalogValid, which fails only on my machine — ~/.spawn/catalog.yaml is a personal overlay pointing chimerax/paraview at private ECR, exactly as #392 prescribes, and the validator checks the merged view. CI has no overlay.

Found while validating #409 against real EC2 — I made the mistake myself.

`--upload` takes a value, so `--upload --region us-east-1` binds "--region"
as the filename and leaves "us-east-1" to be parsed as a positional
argument. The service is handed a stray argv element, the region goes
silently unset, and the dry run prints "Instance: t4g.micro in -" with no
cost line at all.

Three changes, in order of what they cost:

- The path is validated during argument parsing, before the AWS client
  exists. `uploadToInstance` already stat'd the file, but by then an
  instance is running, so a typo cost a launch. Now it costs $0.
- A value starting with "-" is named as a flag rather than reported as
  "no such file or directory: --region", which is technically true and
  buries the lede. A directory is also rejected.
- A dry run with no resolved region says why it can't quote a cost bound
  instead of omitting the line. Silence there reads as "this is free", and
  an unset region is the symptom of this exact mistake, so naming it points
  at the cause.

Also commits docs-gen/service.md, which #409 generated but never committed.
The drift gate diffs tracked files only, so an untracked new fragment slips
through CI and would first fail at tag time, where `make check-docs` runs as
a release guard.

The new test neutralises AWS credentials rather than assuming none are
present: its claim is "this never reaches AWS", and a developer's shell
usually has AWS_PROFILE set. Verified by mutation — with the guard removed
it now fails in 0.00s having contacted nothing, where before it spent 30s
reaching a real account.
@codecov

codecov Bot commented Aug 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@scttfrdmn
scttfrdmn merged commit 99a34bd into main Aug 3, 2026
7 checks passed
@scttfrdmn
scttfrdmn deleted the fix/service-upload-validation branch August 3, 2026 03:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant