Skip to content

Freeze the public contract and cut 1.0.0 - #28

Merged
subalee merged 15 commits into
mainfrom
fix/readiness-audit-v1
Aug 10, 2026
Merged

Freeze the public contract and cut 1.0.0#28
subalee merged 15 commits into
mainfrom
fix/readiness-audit-v1

Conversation

@subalee

@subalee subalee commented Aug 10, 2026

Copy link
Copy Markdown
Member

Cuts 1.0.0, closing out the readiness audit run against 0.3.0.

Every item here was chosen by one criterion: would deferring it cost a major
version?
The CLI's contract — command tree and flags, machine-readable output
shapes, error codes, exit codes, environment variables — is frozen by this release,
so these are the changes that had to land first. The changelog entry states that
freeze explicitly.

The audit's remaining twenty items are gaps, not defects, and every one is additive
— verified rather than assumed — so they can land in 1.x without a major bump.

Contract changes

Error codes are derived from the HTTP status. code previously reported whatever
the API supplied and fell back to HTTP_<status> only when there was none — so one 404
surfaced as 000-004 and another as HTTP_404, and which you got depended on the
endpoint. Those codes appear nowhere in the OpenAPI contract, and the spelling could
change without a CLI release. code is now always HTTP_<status>, and the upstream
value rides along verbatim as apiCode, documented as diagnostic-only.

before  {"code":"000-004","message":"Monitor not found","status":404}
        {"code":"HTTP_404","message":"Monitor not found","status":404}
after   {"apiCode":"000-004","code":"HTTP_404",…}
        {"code":"HTTP_404",…}

This also keeps the door open: if the API ever documents its codes (UPT-3730), they can
be surfaced as a new field with no breaking change.

Exit codes distinguish "don't retry" from "retrying may help". HTTP 400 and 422 now
exit 2, the code locally-caught invalid input already used. HTTP 409 takes a new 8.
All three were previously the generic 1, which remains the fallback for everything
else, including BULK_FAILED.

A typed flag and --set that disagree now error. --name X --set friendlyName=Y
silently discarded the typed flag and created the monitor as Y. They are peers, so the
conflict is now INVALID_INPUT naming both values. --body is unchanged — it stays a
base document both may override.

A heartbeat monitor's url is redacted. It carries the token its ping endpoint is
built from, so anyone holding it can report the heartbeat and keep a dead service looking
alive, with no alert raised. A single monitors list previously put every heartbeat
token on the account into CI logs and agent transcripts. Redaction is keyed on the
monitor's type rather than the field name, so url on every other type still prints as
the target address it is.

Additions

uptimerobot version reports version, platform and Node through the normal output
flags, so CI can assert a minimum version without parsing a sentence. The bare
--version is handled inside oclif before any command runs and keeps its single human
line.

A derived pingUrl on heartbeat monitors. The API returns the ingredients without
ever naming the result, so the URL a heartbeat must receive could only be obtained from
the dashboard — cron monitoring, the use case most native to a CLI, could not be
automated end to end. UPTIMEROBOT_HEARTBEAT_URL overrides the host. Explicitly derived
rather than authoritative: UPT-3726 asks the API to return it, an API-supplied value
takes precedence, and this gives way when the field exists.

--publish-on-status-page on the two incidents comments write commands, generated
from a field the published contract gained.

Fixes and docs

  • Piping to head, head -c or grep -q no longer prints Detected unsettled top-level await to stderr and exits 13. flush() never settles once stdout is an
    EPIPE'd pipe; the broken pipe is now the end of output, keeping any exit code the
    command had already decided on.
  • The three monitors bulk commands document that they report acceptance, not
    completion
    — no operation id, nothing distinguishing queued from applied. An earlier
    draft quoted a 40–60 second delay; UPT-3557 turned out to be a stale cache and was
    fixed on 2026-08-05, so the duration is gone and only the structural part remains.
  • OpenAPI snapshot synced with the published contract.

Two regression guards

test/command-tree-guards.test.ts asserts integrations create and integrations update stay invokable as commands. Both are bare oneOfs; adding a discriminator
(UPT-3731) would generate integrations create slack|webhook|… and, exactly as with
monitors create, remove the bare command. That is additive at the HTTP level but
breaking for the CLI, and openapi:check would not catch it — it compares generated
output to the snapshot, and a routine openapi:update moves both together. Adding the
subcommands is welcome; both paths are expected to coexist, with --set as the untyped
escape hatch.

Review

Three CodeRabbit findings, all reproduced and all fixed in d4dd89f:

  • A derived pingUrl overwrote an API-supplied one, so the authoritative field this
    composition is meant to defer to could never take precedence. It now wins.
  • sameValue compared serialised JSON, so a typed flag and --set carrying the same
    object with keys in a different order were reported as conflicting — a false positive
    on exactly the case the check is meant to allow. Comparison is now structural.
  • The changelog overclaimed that exit 1 means retryable-only; it remains the generic
    fallback, including BULK_FAILED.

Verification

pnpm test 271 passing across 33 files, up from 230. lint, typecheck,
oxfmt --check and openapi:check all clean. npm pack --dry-run carries no .d.ts,
source maps, test/ or research/ — 118 files, 92.7 kB.

Everything was verified against local stub servers or --dry-run; there were no
credentials in this session, so nothing was exercised against the live API.

Filed with the API team

UPT-3726 (heartbeat ping URL) · UPT-3727 (limit on 2 of 12 list endpoints) ·
UPT-3728 (announcements cannot be deleted) · UPT-3729 (incidents cannot be filtered to
open) · UPT-3730 (error code vocabulary undocumented) · UPT-3731 (integrations
discriminator). UPT-3558 was already in progress.

None of them block this release, and none block the frozen contract.

subalee added 13 commits August 10, 2026 11:31
Incident comments gained publishOnStatusPage, so incidents comments create
and update now generate a --publish-on-status-page flag.
Both write the same request body path and --set was applied last, so
--name X --set friendlyName=Y silently discarded the typed flag and created
the monitor as Y. Typed flags and --set are peers, so a conflict is now
INVALID_INPUT naming the field and both values. Identical values still pass,
and --body remains a base document that both may override.
--version is handled inside oclif before any command runs and prints a single
human line, so a CI job asserting a minimum version had to parse a sentence.
The version command reports the same facts through the normal output flags.
flush() never settles once stdout is an EPIPE'd pipe, so piping to head,
head -c, or grep -q printed an unsettled top-level await warning on stderr
and exited 13. The broken pipe is now the end of output, keeping any exit
code the command had already decided on.
monitors bulk pause, start, and update report success for every matched
monitor immediately, but the change takes roughly 40-60 seconds to apply and
nothing in the response distinguishes queued from applied. A pipeline that
pauses monitors then deploys can page on-call for its own deploy. Descriptions
are curated at runtime because the generated ones are guarded by openapi:check.
The code field reported whatever the API supplied and fell back to
HTTP_<status> only when there was none, so one 404 was 000-004 and another
HTTP_404, and the spelling could change without a CLI release. Those codes
appear nowhere in the contract, so code is now always HTTP_<status> and the
upstream value rides along verbatim as apiCode, for diagnostics only.

Exit codes follow the same status: 400 and 422 map to 2 alongside locally
rejected input, and 409 takes 8, so a script can tell a request worth
changing from a failure worth retrying.
POST /integrations is a bare oneOf. Adding a discriminator would generate
integrations create slack|webhook|... and, as with monitors create, remove the
bare command — a breaking change to the command tree that a routine
openapi:update would apply silently, since check-openapi only compares
generated output to the snapshot. Both paths are expected to coexist.
A heartbeat monitor is inert until something pings its URL, and that URL was
obtainable only from the dashboard: the API returns the ingredients without
naming the result, with url holding the token and the endpoint composed as
<host>/m<id>-<token>. Normalized output now carries pingUrl, leaving --raw
untouched, with UPTIMEROBOT_HEARTBEAT_URL to override the host.

Derived rather than authoritative — UPT-3726 asks the API to return the URL
itself, and this should give way to that field.
UPT-3557 was closed on 2026-08-05 by invalidating public monitor caches after
bulk pause/start/update, so the 40-60 seconds this text quoted was a stale read
rather than a slow write, and quoting it now tells pipelines to wait for a
delay that no longer exists.

What survives the fix is structural: the response reports acceptance, carries
no operation id, and cannot distinguish queued from applied. That is what the
help now says, with no duration attached.
url carries the token a heartbeat's ping endpoint is built from, so anyone
holding it — or the pingUrl composed from it — can report the heartbeat and
keep a dead service looking alive, with no alert raised and nothing on the
monitor to show for it. A single monitors list put every heartbeat token on the
account into CI logs and agent transcripts.

Redaction is name-based and deliberately shape-agnostic, but url is a plain
target address on every other monitor type, so this is keyed on the record's
type rather than added to the sensitive-name list. --reveal-secrets shows both
and --raw stays the untouched response.
PATCH /integrations/{id} carries the same bare oneOf as the create operation,
12 variants and no discriminator, so a discriminator added to one is the
natural companion change to the other and would topic-ify both. The guard
covered only create.
Minor rather than patch: the error envelope gained apiCode and changed how code
is derived, HTTP 400/422/409 map to different exit codes, and a heartbeat
monitor's url is now redacted. Each is a changed output shape or exit code,
which is a minor bump while 0.x.
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The 1.0.0 release adds a version command, standardized API errors, heartbeat URL derivation and redaction, typed flag conflict validation, bulk-operation guidance, comment publication fields, and graceful broken-pipe handling.

Changes

CLI 1.0.0 release

Layer / File(s) Summary
Error and comment contracts
openapi/openapi.yaml, src/api/errors.ts, src/lib/types.ts, test/api-errors.test.ts, test/api-operations.test.ts, test/monitors-list.test.ts
Comment schemas add publishOnStatusPage. API errors now expose HTTP-derived codes, optional apiCode values, and distinct exit codes.
Heartbeat curation and request validation
src/lib/response-curation.ts, src/lib/redact-secrets.ts, src/lib/request.ts, src/lib/operation-command.ts, test/heartbeat-*, test/set-conflicts.test.ts
Non-raw heartbeat responses derive pingUrl, heartbeat URLs are redacted, and conflicting typed and --set values fail validation.
Version command and bulk-operation guidance
src/commands/version.ts, src/lib/command-suggestions.ts, src/lib/help-curation.ts, src/lib/operation-command.ts, package.json, test/version.test.ts, test/monitors-bulk.test.ts, test/topic-help.test.ts, test/command-tree-guards.test.ts
The version command reports CLI, platform, architecture, and Node.js runtime data in multiple formats. Bulk monitor help states that writes are accepted before completion and recommends checking state with monitors get.
Output stream handling and release metadata
bin/run.js, test/broken-pipe.test.ts, CHANGELOG.md, README.md, package.json
The CLI handles closed stdout pipes without failing, and release documentation records the 1.0.0 behavior changes.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR addresses some CLI symptoms, but it does not implement the API and OpenAPI requirements in the linked issues [UPT-3730, UPT-3726, UPT-3557, UPT-3731, UPT-3727, UPT-3728, UPT-3729, UPT-3558]. Implement the required API and OpenAPI changes, or split the CLI release work into separate issues with accurate links.
Out of Scope Changes check ⚠️ Warning The PR includes unrelated CLI changes, including version output, incident comment flags, broken-pipe handling, and response curation, beyond the linked issue scope. Split unrelated CLI features into separate pull requests or link them to issues that define their requirements.
Docstring Coverage ⚠️ Warning Docstring coverage is 22.73% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: freezing the public CLI contract and preparing the 1.0.0 release.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/readiness-audit-v1
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch fix/readiness-audit-v1

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CHANGELOG.md`:
- Around line 48-54: Update the changelog entry describing exit code 1 so it
does not claim the code is exclusively for retryable transient faults. Scope
that statement to unmapped HTTP failures or describe it as the generic fallback,
while preserving the documented BULK_FAILED all-failed bulk behavior.

In `@src/lib/request.ts`:
- Around line 171-176: Update sameValue in src/lib/request.ts to compare objects
structurally or through canonicalized, recursively sorted keys so equivalent
JSON values match regardless of property order; preserve existing handling for
primitives, nullish values, and arrays. Add a regression case in
test/set-conflicts.test.ts confirming matching differently ordered object values
are allowed.

In `@src/lib/response-curation.ts`:
- Around line 20-27: Update withDerivedFields so plain records with an existing
string pingUrl are returned unchanged before deriving heartbeatPingUrl; continue
deriving the field only when no authoritative string value is present, and add a
regression case in heartbeat-ping-url.test.ts.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: bc85d692-b6f2-44c9-a7bc-5ab38b735dca

📥 Commits

Reviewing files that changed from the base of the PR and between 48cf89d and 81e99be.

⛔ Files ignored due to path filters (1)
  • src/generated/operations.ts is excluded by !**/generated/**
📒 Files selected for processing (25)
  • CHANGELOG.md
  • README.md
  • bin/run.js
  • openapi/openapi.yaml
  • package.json
  • src/api/errors.ts
  • src/commands/version.ts
  • src/lib/command-suggestions.ts
  • src/lib/help-curation.ts
  • src/lib/operation-command.ts
  • src/lib/redact-secrets.ts
  • src/lib/request.ts
  • src/lib/response-curation.ts
  • src/lib/types.ts
  • test/api-errors.test.ts
  • test/api-operations.test.ts
  • test/broken-pipe.test.ts
  • test/command-tree-guards.test.ts
  • test/heartbeat-ping-url.test.ts
  • test/heartbeat-secrets.test.ts
  • test/monitors-bulk.test.ts
  • test/monitors-list.test.ts
  • test/set-conflicts.test.ts
  • test/topic-help.test.ts
  • test/version.test.ts

Comment thread CHANGELOG.md Outdated
Comment thread src/lib/request.ts
Comment thread src/lib/response-curation.ts
…ison

An API-supplied pingUrl was overwritten by the derived one, so the field this
composition is meant to defer to could never take precedence. It now wins.

sameValue compared serialised JSON, so a typed flag and --set carrying the same
object with its keys in a different order were reported as conflicting — a false
positive on exactly the case the check is meant to let through. Comparison is
now structural.

Also scopes the changelog's exit-code claim: 1 remains the generic fallback,
including BULK_FAILED, rather than meaning retryable-only.
Cuts the first stable release rather than another 0.x: the contract items from
the readiness audit have all landed, so the command tree, output shapes, error
codes, exit codes, and environment variables are ready to be held to. The
changelog entry states that freeze explicitly.
@subalee subalee changed the title Close the v1 contract items from the 0.3.0 readiness audit (0.4.0) Freeze the public contract and cut 1.0.0 Aug 10, 2026
@subalee
subalee merged commit 36a9121 into main Aug 10, 2026
3 checks passed
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