Skip to content

fix(cli): mops outdated exit codes, and report GitHub deps - #734

Merged
Kamirus merged 1 commit into
v3from
kamil-claude/v3-outdated-exit-code
Aug 12, 2026
Merged

fix(cli): mops outdated exit codes, and report GitHub deps#734
Kamirus merged 1 commit into
v3from
kamil-claude/v3-outdated-exit-code

Conversation

@Kamirus

@Kamirus Kamirus commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

mops outdated exited 0 whether or not anything was outdated, so it could not gate CI. pnpm outdated exits 1; cargo outdated has --exit-code.

$ mops outdated
base 0.14.5 -> 0.14.14
Before: $? = 0
After:  $? = 1

Exit codes are now 0 up to date, 1 updates available, 2 the check could not be completed — missing mops.toml, unknown [pkg], or a registry/GitHub lookup failure. 2 wins when both apply, because a partial report must never read as a clean bill of health, and a failed lookup must never read as "outdated". 1 for "found something" and 2 for "failed to look" follows the grep/diff convention and matches npm outdated.

The exit code was only half the problem

getAvailableUpdates filtered to dependencies with a truthy version, which silently excludes GitHub (repo) dependencies — but mops update does update them, by re-resolving the branch head and rewriting the pin. So the two commands disagreed about what "outdated" means:

# mops.toml:  mydep = "https://github.com/org/repo#master@1111111"
#             (branch has since moved to 06d7c77)

$ mops outdated
Before: All dependencies are up to date!     # then `mops update` rewrites the pin
After:  mydep 1111111 -> 06d7c77 (github: org/repo#master)

That's the defect that made the exit code useless: a green mops outdated didn't mean a subsequent mops update would be a no-op. Both commands now apply the same rule — same helper shape, same [pkg] filter, same "sha differs from branch head" comparison — so they can't drift apart again.

A GitHub lookup that fails (rate limit, network) reports per-dependency and exits 2. It never degrades to a silent "up to date".

Cost

One GitHub API call per repo dependency, and most projects have none. They're issued concurrently, unlike mops update's serial loop. Against that, the registry round trip is now skipped entirely when a project has no registry dependencies, so a GitHub-only project makes strictly fewer calls than before.

No auth-token support and no --no-github escape hatch: neither npm nor cargo has an analogue, and inventing one before anyone hits the unauthenticated 60 req/h limit seemed premature.

What is unchanged

mops update's behaviour is byte-for-byte identical. getAvailableUpdates gained a fourth optional argument for opting into throwing instead of process.exit(1), and only outdated passes it, so update keeps its existing print-and-exit path.

getHighestSemverBatch's assert(list.size() < 100) is untouched and unaffected — repo deps were never in that batch, and passing [pkg] only shrinks it. It remains a live trap for projects with 100+ direct dependencies, tracked as item 26 in #723.

Follow-up worth flagging

mops update <not-installed-pkg> still exits 0 while mops outdated <not-installed-pkg> now exits 2. Worth aligning update to 2. And mops update still has its own inline GitHub resolution loop that duplicates the rule this PR extracted into a helper — a clean refactor now that the helper exists, but out of scope here since it would put update.ts in the diff.

Part of the GA follow-up work from the Cargo/pnpm audit (#723, item 17), which the triage there marks as pre-GA because exit codes are a contract.

🤖 Generated with Claude Code

`mops outdated` exited 0 whether or not anything was outdated, so it could not
gate CI. It now exits 1 when updates are available and 2 when the check itself
could not be completed — no mops.toml, unknown package, registry or GitHub
lookup error — so a partial report is never mistaken for a clean bill of health.
1 for "found something" matches npm outdated and pnpm outdated.

The exit code was only half the problem. `getAvailableUpdates` filtered to deps
with a truthy version, which excludes GitHub deps, while `mops update` does
update them by re-resolving the branch. So `outdated` could print "All
dependencies are up to date!" for a project where `mops update` would rewrite a
GitHub pin. Both now apply the same rule, so they cannot disagree.

Also wires the `[pkg]` argument that `getAvailableUpdates` already accepted,
matching `mops update [pkg]`, and skips the registry round trip entirely when a
project has no registry dependencies.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Kamirus Kamirus added the cli label Aug 12, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Cursor AI review

👍 APPROVE — looks safe to merge

Verified GitHub outdated logic against update.ts’s loop (parseGithubURL + getGithubCommit + sha compare), confirmed throwOnError leaves update on process.exit(1), and checked docs/changelog/skill/--help stay aligned.

Category Assessment Details
Summary Makes mops outdated a real CI gate (exit 0/1/2) and reports GitHub deps with the same branch-head rule as mops update, plus optional [pkg].
Code Quality Reuses getGithubCommit/parseGithubURL; GitHub filter mirrors update.ts lines 39–44; throwOnError is opt-in so update’s exit path is unchanged.
Consistency outdated [pkg] matches update [pkg]; --help, docs/docs/cli/1-deps/03-mops-outdated.md, cli/CHANGELOG.md ## Next, and .agents/skills/mops-cli/SKILL.md updated together.
Security No auth/integrity changes; GitHub calls are the same unauthenticated API already used by update.ts / getGithubCommit in cli/mops.ts.
Tests New cli/tests/outdated.test.ts covers exit 0/1/2, registry skip, GitHub move/up-to-date/unpinned/failure (incl. 2 over 1), and [pkg] filtering via targeted assertions.
Maintainability Shared getAvailableGithubUpdates prevents outdated/update drift; remaining inline loop in update.ts is explicitly deferred and not a defect here.

Verdict

Decision: APPROVE
Risk: Low
Reason: Contained, well-tested CLI contract fix outside registry/install/auth paths; exit-code and GitHub-reporting changes are intentional, documented, and aligned with mops update and npm/pnpm conventions.


Generated for commit 2f45573

@automation-sa-sre automation-sa-sre left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated approval: the AI review verdict for 2f45573 is APPROVE. See the "Cursor AI review" comment for details.

@Kamirus
Kamirus merged commit 3ccf18b into v3 Aug 12, 2026
21 checks passed
@Kamirus
Kamirus deleted the kamil-claude/v3-outdated-exit-code branch August 12, 2026 14:37
Kamirus added a commit that referenced this pull request Aug 13, 2026
…oml (#742)

Three consistency gaps between `mops update` and `mops outdated`, all of
which make a scripted update quietly misbehave.

## `mops update` reported failure and exited 0

```
$ mops update no-such-package
  Package "no-such-package" is not installed!
Before: $? = 0
After:  $? = 2
```

A typo in a scripted update looked like success. It now exits `2` — the
same code `mops outdated` uses for "the check could not be completed"
([#734](#734)) — for an unknown
package and for a missing `mops.toml`. `1` is deliberately *not* reused:
it means "updates are available", which cannot be a terminal state for a
command whose job is to apply them.

## The two commands could disagree about GitHub deps

#734 extracted `getAvailableGithubUpdates` precisely so `outdated` and
`update` could not drift apart on what "out of date" means for a `repo =
"..."` dependency — but `update` kept its own inline loop resolving
branch heads, so the rule still existed twice. It now uses the helper.
The helper needed no changes: it already carries the repo, branch,
resolved head and dependency name, which is exactly enough to rebuild
the pin URL.

Behaviour is preserved deliberately and specifically: the same `add(url,
{ dev, lock: "skip" }, name)` call, `dev` still derived from where the
dependency is declared, the alias key still passed through as the
manifest key, and a failed lookup still prints and lets the rest of the
run continue. Two visible nuances: lookups now run concurrently rather
than sequentially (same request count against GitHub's 60/hour limit),
and lookup errors print after the re-pins rather than interleaved with
them.

## Neither `--help` nor the docs said it rewrites your manifest

`mops update` is `cargo upgrade` semantics — it rewrites `mops.toml` —
not `cargo update`, which only touches the lockfile.
[#723](#723) records the
decision to keep the name (under exact pins there is only one update
operation that can exist, so there is no ambiguity to resolve, and
renaming would be breaking). The agreed remedy was to say so plainly,
which neither the help text nor the doc page did:

```
$ mops update --help
Rewrite the versions in mops.toml to the highest semver-compatible ones within
the caret bound (does not cross major versions, or pre-1.0 minor versions)

Rewrites the new versions into mops.toml, and keeps mops.lock in sync.
GitHub dependencies are re-pinned to their branch head (one GitHub API call each).

Exit codes:
  0  mops.toml is up to date
  2  the update could not be run (no mops.toml, unknown [pkg])
```

## Follow-up worth flagging

One divergence of the same family is left in place: `update` still exits
`0` when a *GitHub lookup* fails — it prints `Failed to update <name>:
…` and carries on — where `outdated` exits `2`. Changing it would alter
behaviour beyond the scope of aligning the "not installed" case, so it
is called out rather than folded in.

Items 35, 36 and 37 in
[#723](#723).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants