refactor: use @heroku/sdk for apps:info command#3749
Conversation
…drop nock Replaces the nock-based HTTP interception with prototype stubs on HerokuSDK.platform (for the platform.app.describe call) and on HerokuApiClient.get (for the --extended escape-hatch call). This keeps the tests asserting on the SDK contract the migrated command actually depends on, rather than on URL shape.
…tch guidance
Captures lessons from the apps:info migration:
- Pre-flight P1: replace the broken Node diagnostics. The SDK's exports
map blocks `require('@heroku/sdk/package.json')` and the entry's
top-level await blocks `require('@heroku/sdk')`. Read the installed
package.json directly and use `node --input-type=module` for the
HerokuSDK probe.
- Pre-flight P4 (new): look for composite resource methods before
running the codemod. The codemod walks the route table and can never
suggest composites like `platform.app.describe`, but they collapse
Promise.all-of-N-endpoints patterns into a single call with built-in
soft-fail behavior. Note the snake_case / camelCase mapping that
composites often require to preserve CLI output contracts.
- Step 1.2b (new): document the `HerokuApiClient` escape hatch from
`@heroku/heroku-fetch` for CLI-only query variants (e.g.,
?extended=true) the SDK doesn't expose. Preferable to keeping a
`this.heroku.<verb>` thread because it cleanly removes the APIClient
dependency and stubs at one prototype boundary in tests.
- Step 2.2b (new): parallel test-stub recipe for the escape hatch.
Stub HerokuApiClient.prototype.get and use a duck-typed
`{json: async () => fixture}` return — avoids the
n/no-unsupported-features/node-builtins warning on `new Response()`.
- Step V3: codify `feat/heroku-sdk-integration` as the base branch for
all per-command migration PRs (not main). Avoids surfacing
~120 files of integration-branch noise to reviewers.
- Tech-stack section: SDK is now a published beta (^0.1.0-beta), no
longer tracking GitHub main. Added @heroku/heroku-fetch as a direct
dep.
- Self-review checklist: add composite-method check, escape-hatch test
shape, and base-branch verification.
- Glossary: composite method, escape hatch, integration branch.
The previous commit constructed the SDK with no extensions, so platform.app.describe was undefined at runtime — production would have hit a TypeError on the first invocation. Stub-based tests didn't catch it because the test fixtures define `app.describe` directly on the fake platform, bypassing the missing extension entirely. Also extends the SDK migration skill: P4 now spells out that composite methods are extensions and must be registered on the SDK constructor, and a new self-review item verifies the wiring. Lists the extension exports currently used in this repo (apps, addons, pipeline-coupling, database, postgres-database, log-session) to make the right choice discoverable at migration time.
The bare `type Platform = HerokuSDK['platform']` indexes the class's
prototype-level getter without supplying the `Exts` generic, so the
type falls back to its default empty tuple and `describe` is missing
from the resolved type. Runtime wiring was correct (the constructor
got `{extensions: [appExtensions]}`), but the helper's parameter type
didn't see the extension. Parameterizing the alias as
`HerokuSDK<readonly [typeof appExtensions]>['platform']` aligns the
helper's static type with the constructed SDK's actual shape.
Why this wasn't caught earlier: the skill's tsc-delta verification
recipe (`grep -v -F -f /tmp/tsc-baseline.txt`) outputs nothing when
the baseline file is empty, because grep treats an empty pattern file
as "no patterns to match" and -v inverts that to "exclude everything"
— a false "no errors" signal in the clean-baseline case.
Skill updates:
- Step 1.3 and V2: guard the baseline filter with `[ -s … ]` so an
empty baseline falls back to running tsc unfiltered.
- Step P4: extended composite-method guidance with the type-level
threading recipe — `type Platform = HerokuSDK<readonly [typeof
appExtensions]>['platform']` for helpers that call extension methods.
…d SDK composites @heroku-cli/schema is deprecated. Switches the file to: - Entity types (`App`, `AddOn`, `Collaborator`, `Dyno`) from `@heroku/types/3.sdk`. - Composite return types (`AppInfo`, `PipelineCouplingDetail`) from `@heroku/sdk/resources/platform/app/info`. - A local `LocalApp` extension that captures fields the platform returns but `@heroku/types/3.sdk` doesn't declare yet (`cron_finished_at`, `cron_next_run`, `database_size`, `create_status`, `extended`). Makes the schema gap visible and reviewable rather than papering over with `as any`. - A local `Info` type for the helper return shape (snake_case `pipeline_coupling`) that mirrors what print/shell consume, replacing the previous misuse of `Heroku.App` as a struct holder. Strict-null fields (`web_url`, `repo_size`, `slug_size`) reach non-null-accepting consumers (`filesize`, `color.info`); cast at the call sites to preserve the original behavior of printing the value literally when null. The local `print(k, v)` inside `shell()` is loosened from `(k: string, v: string)` to `(k: string, v: unknown)` since runtime is template-literal interpolation, which preserves the existing behavior of printing booleans, arrays, and undefined values verbatim. Skill updates: - Tech stack: call out @heroku-cli/schema as deprecated. - Step 1.2c (new): replacement playbook covering entity types, composite return types, and the local-extension recipe for fields the strict schema doesn't declare. Documents the safe handling of strict-null findings (no silent `?? ''` coalescing — preserve observable output). - Self-review checklist: replace the "no schema import if unused" item with a stronger "no schema import, period" plus a strict-null preservation check. - Glossary: distinguish entity types vs. composite return types and their import sources.
… lib/types
Replace the field-by-field Info copy with `Omit<AppInfo, 'app'> & {app: ExtendedApp}`
so new SDK fields propagate automatically. Localize the
`pipelineCoupling` -> `pipeline_coupling` rename to the JSON output
boundary instead of threading it through internal helpers.
Promote `LocalApp` -> `ExtendedApp` into `src/lib/types/app.d.ts` next to
the existing `App`/`Apps` exports so future migrations can reuse it.
… in SDK migration skill Add two principle subsections to Step 1.2c capturing patterns that will recur across command migrations: - Derive composite-shaped helper types from the SDK rather than restating them; field-by-field copies type-check but break at runtime if the SDK renames a field upstream. - Localize CLI/SDK contract translations (snake_case <-> camelCase) to the output boundary instead of threading renamed fields through internal helpers. Also redirect bullet 3 of Step 1.2c to `src/lib/types/` first (with `ExtendedApp` named explicitly), and frame local extension type declarations as transient stepping stones promoted on second use.
8953abd to
04ea66b
Compare
…date Signed-off-by: Timothy Lowrimore <154477569+tlowrimore-heroku@users.noreply.github.com>
michaelmalave
left a comment
There was a problem hiding this comment.
I think its unclear now how tight we're working with the SDK at this point? I think there are some things we should consider for the SDK but not necessarily blocking because clean migration otherwise. Nice.
| print('web_url', info.app.web_url) | ||
| print('repo_size', filesize(info.app.repo_size, {round: 0, standard: 'jedec'})) | ||
| if (getGeneration(info.app) !== 'fir') print('slug_size', filesize(info.app.slug_size, {round: 0, standard: 'jedec'})) | ||
| print('web_url', info.app.web_url as string) |
There was a problem hiding this comment.
We're casting web_url as string, repo_size as number, slug_size as number because @heroku/security-starter-projects defines them as T | null. The command assumes they're always populated for an existing app. Should we add a DescribedApp narrowed type in the SDK's info.ts that asserts these fields are non-null post-fetch (since describe only returns real apps, not create-in-progress shells), so CLI commands don't need as casts?
| async function getInfo(app: string, platform: Platform, extended: boolean): Promise<Info> { | ||
| const data: Info = await platform.app.describe(app) | ||
|
|
||
| if (extended) { |
There was a problem hiding this comment.
We're using HerokuApiClient directly for the extended endpoint. Since the SDK already owns the composite describe, should describe accept an extended?: boolean option, fetch ?extended=true internally, and merge the result — pushing that logic out of the CLI?
There was a problem hiding this comment.
I went back and forth about this, and landed on this being uniquely a CLI concern, as I understand it. The other thought is that SDK will be customer-facing and this extended flag pertains only to sudo users.
| export type App = BaseApp & Pick<TeamApp, 'locked' | 'joined'> | ||
| export type Apps = App[] | ||
|
|
||
| export type ExtendedApp = App & { |
There was a problem hiding this comment.
We're defining ExtendedApp locally with create_status, cron_*, database_size, extended fields. If the SDK's describe gains the extended option, these fields belong on the SDK's return type. Should we move this type into @heroku/sdk or @heroku/types so it's the SDK's contract, not a CLI-local workaround?
There was a problem hiding this comment.
See my previous note on the extended arg to the describe function.
3ec2f64
into
feat/heroku-sdk-integration
Summary
src/commands/apps/info.tsto callplatform.app.describe()from@heroku/sdkin place of the inlinePromise.allthat fanned out raw API calls for add-ons, the app, dynos, collaborators, and the pipeline coupling. The SDK composite encapsulates the same fan-out plus the soft-fail behavior on the optional resources, so ~25 lines of orchestration collapse to a single call.@heroku/heroku-fetchHerokuApiClientcall for the--extendedpath. The?extended=truequery variant is a CLI-only concern not exposed through the SDK, and the user explicitly endorsed this hybrid approach during planning.@heroku/heroku-fetchis added as a direct dependency in this PR (it was already on disk as a transitive dep of@heroku/sdk).test/unit/commands/apps/info.unit.test.tsto dropnockentirely. SDK calls are stubbed viaHerokuSDK.prototype.platform; the--extendedescape-hatch call is stubbed viaHerokuApiClient.prototype.get. This keeps tests asserting on the SDK contract the migrated command depends on, rather than on URL shape.The
pipelineCouplingfield returned by the SDK is mapped back to snake-case (pipeline_coupling) before output to preserve the existing JSON-output contract that the tests already assert on.Test plan
npx tsc --noEmit -p tsconfig.json— no new errors vs. baseline.npx eslint src/commands/apps/info.ts test/unit/commands/apps/info.unit.test.ts— clean (the two pre-existing warnings onprint()'s 5-param signature are unchanged).npx mocha 'test/unit/commands/apps/info.unit.test.ts'— 12 passing.npx mocha 'test/unit/commands/apps/**/*.unit.test.ts'— 89 passing, no sibling regressions.heroku apps:info,heroku apps:info --shell,heroku apps:info --json,heroku apps:info --extended, andheroku apps:info --extended --jsonagainst a real app before merging.