refactor: use @heroku/sdk for pipelines commands#3717
Merged
eablack merged 3 commits intoMay 20, 2026
Conversation
Replace direct Platform API calls in pipelines (list), pipelines:create, pipelines:info (via disambiguate), and pipelines:promote with @heroku/sdk equivalents: - pipeline.list() / pipeline.info() / pipeline.create() - pipelineCoupling.create() - account.infoByUser() / team.info() - promotePipeline() composition with onReleaseStream callback The release-command output streaming previously implemented inline in promote.ts now flows through the SDK's onReleaseStream hook, which hands a web ReadableStream to the CLI to pipe to stdout. The local poll/stream/2FA helpers are removed. Promote.promotePipeline is exposed as a static reference so tests can stub the SDK call (matches the prior Cmd.sleep convention). Adds a local PipelineCreateBody type that extends @heroku/types' PipelineCreateOpts with the undocumented `generation` request field the platform accepts but the schema doesn't declare. Adds tmp/ to the eslint ignore list.
…tion - pipelines:destroy now calls SDK pipeline.delete(). - pipelines:update now calls SDK pipelineCoupling.infoByApp() + pipelineCoupling.update(). - Drop the lib/api.ts listPipelineApps wrapper and have the four consumers (info, diff, transfer, promote) import the SDK composition directly. AppWithPipelineCoupling type also moves to the SDK; render-pipeline.ts widens it locally to satisfy hux.table's Record<string, unknown> row constraint. - Removes the now-unused getAppFilter helper, listCouplings helper, and FILTERS_HEADER constant from lib/api.ts. - Bumps @heroku/sdk to the eb/feat/list-pipeline-apps branch which exposes the new listPipelineApps composition.
eablack
added a commit
that referenced
this pull request
May 21, 2026
The integration branch's #3717 imports from @heroku/sdk/compositions/pipeline, but the SDK's exports cleanup (heroku-sdk#26) replaced compositions/ with resources/. Update each call site to use the new shape: - listPipelineApps → pipelineCouplingExtensions.listApps via HerokuSDK - promotePipeline → standalone import (kept as a static reference on the Promote command class so existing sinon stubs continue to work) - AppWithPipelineCoupling, ReleaseStreamContext → type imports from resources/platform/pipeline-{coupling,promotion} The promote test's stub callback now sees a 3-arg signature (ctx, body, options) instead of 2-arg, and firstCall.args[0] becomes [1] for body assertions. Test scope cleanup for addons/index and addons/info: split the 'apiSdk' nock scope (Accept-Expansion required) from the 'api' scope (no expansion). Global /addons and /addons/<id>/addon-attachments don't accept the expansion header, matching the SDK's per-call header scoping in heroku-sdk#27.
eablack
added a commit
that referenced
this pull request
May 22, 2026
* refactor: use @heroku/sdk for addons list and addons:info
- addons (list) now calls SDK addOn.list / addOn.listByApp and
addOnAttachment.list / addOnAttachment.listByApp. The
Accept-Expansion: addon_service,plan header that drives nested
service/plan inlining is now passed once via createPlatformClient
options.
- addons:info now uses addOnAttachment.listByAddOn for the attachment
fetch. resolveAddon keeps its existing /actions/addons/resolve flow
and cache.
* refactor: use @heroku/sdk for addons commands
Replace direct Platform API calls in addons (list), addons:info,
addons:create, addons:services, addons:plans, and addons:upgrade
with @heroku/sdk equivalents:
- addOn.list / addOn.listByApp + addOnAttachment.list / listByApp
- describeAddon (resolves, fetches attachments, applies grandfathered
pricing in one call)
- addOn.create
- addOnService.list
- plan.listByAddOn
- upgrade composition (resolves + updates in one call, with the
onResolved callback firing between for the action display line)
Drops legacy lib/addons/resolve.ts dependency from upgrade in favor
of the SDK's typed AddonAmbiguousError. Tests updated for the SDK's
body/header/error shapes (UUIDs in PATCH paths, no `app: null` on
resolve bodies, etc.).
Adds tmp/ to the eslint ignore list (build artifacts produced 138k
unrelated lint errors).
* refactor: migrate addons/maintenance commands to @heroku/sdk extensions
The SDK was rearchitected to replace the compositions/ helpers with
a HerokuSDK + extension factory pattern. Update each command to
construct a HerokuSDK with the extensions it needs and call methods
through the resulting platform proxy.
Also bumps the SDK pin to the working branch so addOnExtensions.upgrade
exposes onResolved (previously dropped from the extension's options
type).
* refactor: migrate rename/detach/plans to @heroku/sdk
- rename: addOn.info + addOn.update (passes plan through unchanged
to satisfy the schema's required field)
- detach: addOnAttachment.infoByApp + addOnAttachment.delete +
release.list (Range header via withHeaders)
- plans: addOn.listPlans extension (replaces lodash sort with native
contract+cents sort)
Test fixtures updated to return JSON bodies for the PATCH/DELETE
responses the SDK now parses.
* refactor: migrate addons:create flow to @heroku/sdk + fix stale tests
- waitForAddonProvisioning: use createPlatformClient + addOn.infoByApp
with Accept-Expansion header via withHeaders. Drops APIClient param.
- createAddon helper: drops APIClient param (no longer needed once
waitForAddonProvisioning is migrated).
- create.ts, wait.ts, data:pg:{create,fork,migrate}: update call sites
to match the new signature (no this.heroku passed in).
- addons:wait test: replace lolex install + setTimeout override with
sinon.useFakeTimers({toFake: ['Date'], shouldAdvanceTime: true})
so the SDK's real setTimeout polling drives the test while Date.now
can still be fake-ticked for the >5s notifier threshold.
- pg create/fork test fixtures: nock body matchers changed from
{plan: {name: 'foo'}} to {plan: 'foo'} to match the canonical
AddOnCreateOpts shape the SDK sends.
- migrate test: shift createAddonStub argument indexes to reflect the
new signature (heroku param removed).
* refactor: use addOn.createAndWait for create-addon helper
Replaces the local trapConfirmationRequired + waitForAddonProvisioning
flow with a single platform.addOn.createAndWait call. The SDK now owns:
- 423 confirmation_required → typed AddonConfirmationRequiredError,
caught here and passed through ConfirmCommand for the UX prompt.
- state=provisioning + wait=true → poll loop until terminal.
- state=deprovisioned → typed AddonProvisioningFailedError.
The two-phase status display (Creating <plan>... <price>, then
Creating <addonName>... done while polling) is preserved by hooking
the SDK's onProvisioning callback to close the create-phase action,
print the provision message + 'Waiting for...' line, and start the
wait-phase action.
Bumps SDK pin to eb/feat/addon-create-and-wait.
* refactor: import HerokuSDK from '@heroku/sdk' root
Per @heroku/sdk#26 (chore!: cleans-up exports), HerokuSDK is now
exported from the package root and the './sdk' subpath is gone.
Bump the lockfile to pick up the SDK 0.4.0 build that includes both
this exports change and the createAndWait + onProvisioning callback
work that lib/addons/create-addon.ts depends on.
* fix(addons): scope Accept-Expansion to per-app addon list calls
The global /addons endpoint rejects Accept-Expansion: addon_service,plan
('must be within ``'), but the per-app /apps/:id/addons endpoint
accepts it. Move the expansion off createPlatformClient's defaults and
onto a withHeaders-scoped client used only for the app-scoped list.
The attachment endpoints (list / listByApp) use Accept-Inclusion, not
Accept-Expansion, so they don't need the header either.
Bumps the SDK pin to pick up describeAddon's matching expansion-scoping
fix.
* fix(pipelines): migrate compositions/pipeline imports to resources/
The integration branch's #3717 imports from @heroku/sdk/compositions/pipeline,
but the SDK's exports cleanup (heroku-sdk#26) replaced compositions/ with
resources/. Update each call site to use the new shape:
- listPipelineApps → pipelineCouplingExtensions.listApps via HerokuSDK
- promotePipeline → standalone import (kept as a static reference on
the Promote command class so existing sinon stubs continue to work)
- AppWithPipelineCoupling, ReleaseStreamContext → type imports from
resources/platform/pipeline-{coupling,promotion}
The promote test's stub callback now sees a 3-arg signature (ctx, body,
options) instead of 2-arg, and firstCall.args[0] becomes [1] for body
assertions.
Test scope cleanup for addons/index and addons/info: split the 'apiSdk'
nock scope (Accept-Expansion required) from the 'api' scope (no
expansion). Global /addons and /addons/<id>/addon-attachments don't
accept the expansion header, matching the SDK's per-call header
scoping in heroku-sdk#27.
* chore: pin @heroku/sdk to main + apply auto-fix import sorts
heroku-sdk PR #27 has merged; flip the pin from the feature branch
back to main. Resolves to commit efce1d4.
Also pulls in the eslint --fix import-sort cleanup across the seven
files we touched (each was importing '@heroku/sdk/extensions/platform'
or '@heroku/sdk/resources/platform/...' before '@heroku/sdk', which
the perfectionist/sort-imports rule flagged as an error).
* refactor: standardize SDK access on HerokuSDK + extensions
Per review feedback on #3718: the PR mixed two patterns for getting a
reference to the platform service — some files used createPlatformClient,
others used new HerokuSDK({extensions: [...]}).platform. Pick the
HerokuSDK form everywhere so there's one shape in the codebase.
For files that don't need any extension methods (detach, index/list,
rename, services, maintenance/index, addons-wait), HerokuSDK is
constructed without an extensions array — the resulting platform proxy
exposes all the route-registry methods exactly the same as
createPlatformClient.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
pipelines(list),pipelines:create,pipelines:info(viadisambiguate's UUID branch),pipelines:promote,pipelines:destroy, andpipelines:updatewith@heroku/sdkequivalents (pipeline.list/info/create/delete,pipelineCoupling.create/infoByApp/update,account.infoByUser,team.info,promotePipeline).pipelines:promotetopromotePipeline'sonReleaseStreamcallback. The release-command output streaming logic that lived in this file (busl fetch with retry, single-target early-exit, release info lookup) now lives in the SDK; the CLI just pipes the resultingReadableStream<Uint8Array>toprocess.stdout. Drops ~80 lines of poll/stream/2FA helpers.listPipelineAppswrapper fromsrc/lib/api.tsand have its four consumers (info,diff,transfer,promote) import the new SDK composition directly. TheAppWithPipelineCouplingtype also moves to the SDK;render-pipeline.tswidens it locally withRecord<string, unknown>to satisfyhux.table's row constraint. Removes the now-unusedgetAppFilter/listCouplingshelpers andFILTERS_HEADERconstant fromlib/api.ts.PipelineCreateBodytype that extends@heroku/types'PipelineCreateOptswith the undocumentedgenerationfield the API accepts but the heroku/api schema doesn't declare. Comment in source explains why.Promote.promotePipelineas a static reference so tests can stub the SDK call without changing production code (mirrors the priorCmd.sleepconvention).tmp/**/*to the eslint ignore list (build artifacts were producing 138k unrelated lint errors).Type of Change
Testing
Notes: Smoke-tested against real apps in the
heroku-dev-toolsteam using a locally built CLI. Both promotion paths exercised: standard polling and the single-target release-command streaming path (the more complex behavior the SDK'sonReleaseStreamcallback now drives).updateanddestroyexercised against the same pipeline.Steps:
npm run buildto compile the refactored CLI.pipelines:updatere-stages an app:/sourcesfor a signed S3 pair, upload a small tarball containing aProcfilewith bothweb:andrelease:processes (release script prints lines over ~10s so the streaming path is observable), then POST/apps/eb-sdk-test-staging/buildswith theget_url. Wait for statussucceeded.Running release command...followed by the streamedstep 1 / 10...step 10 / 10lines emitted in real time, thenPromotion successfulandeb-sdk-test-prod: succeeded. This exercisesHerokuApiClient.stream()→ SDKonReleaseStream→stream.pipeTo(WritableStream)→process.stdout.pipelines:destroyremoves the pipeline:heroku apps:destroyfor both apps.Related Issues
GUS work item: W-22265744