Skip to content

refactor: use @heroku/sdk for pipelines commands#3717

Merged
eablack merged 3 commits into
feat/heroku-sdk-integrationfrom
eb/refactor/sdk-pipelines-commands
May 20, 2026
Merged

refactor: use @heroku/sdk for pipelines commands#3717
eablack merged 3 commits into
feat/heroku-sdk-integrationfrom
eb/refactor/sdk-pipelines-commands

Conversation

@eablack
Copy link
Copy Markdown
Contributor

@eablack eablack commented May 18, 2026

Summary

  • Replace direct Platform API calls in pipelines (list), pipelines:create, pipelines:info (via disambiguate's UUID branch), pipelines:promote, pipelines:destroy, and pipelines:update with @heroku/sdk equivalents (pipeline.list/info/create/delete, pipelineCoupling.create/infoByApp/update, account.infoByUser, team.info, promotePipeline).
  • Wire pipelines:promote to promotePipeline's onReleaseStream callback. 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 resulting ReadableStream<Uint8Array> to process.stdout. Drops ~80 lines of poll/stream/2FA helpers.
  • Drop the listPipelineApps wrapper from src/lib/api.ts and have its four consumers (info, diff, transfer, promote) import the new SDK composition directly. The AppWithPipelineCoupling type also moves to the SDK; render-pipeline.ts widens it locally with Record<string, unknown> to satisfy hux.table's row constraint. Removes the now-unused getAppFilter/listCouplings helpers and FILTERS_HEADER constant from lib/api.ts.
  • Add a local PipelineCreateBody type that extends @heroku/types' PipelineCreateOpts with the undocumented generation field the API accepts but the heroku/api schema doesn't declare. Comment in source explains why.
  • Expose Promote.promotePipeline as a static reference so tests can stub the SDK call without changing production code (mirrors the prior Cmd.sleep convention).
  • Add tmp/**/* to the eslint ignore list (build artifacts were producing 138k unrelated lint errors).

Type of Change

  • refactor: Refactoring existing code without changing behavior

Testing

Notes: Smoke-tested against real apps in the heroku-dev-tools team 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's onReleaseStream callback now drives). update and destroy exercised against the same pipeline.

Steps:

  1. npm run build to compile the refactored CLI.
  2. Create two staging/prod apps:
    heroku apps:create eb-sdk-test-staging --team heroku-dev-tools
    heroku apps:create eb-sdk-test-prod --team heroku-dev-tools
    
  3. Build the pipeline using the refactored commands:
    ./bin/run pipelines:create eb-sdk-test-pipeline -a eb-sdk-test-staging -s staging -t heroku-dev-tools
    ./bin/run pipelines:add eb-sdk-test-pipeline -a eb-sdk-test-prod -s production
    ./bin/run pipelines:info eb-sdk-test-pipeline   # confirms the assembled pipeline (renders apps via SDK listPipelineApps)
    ./bin/run pipelines                              # confirms list output
    
  4. Verify pipelines:update re-stages an app:
    ./bin/run pipelines:update -a eb-sdk-test-staging -s production
    ./bin/run pipelines:info eb-sdk-test-pipeline   # confirms eb-sdk-test-staging now shows in production
    
  5. Seed the staging app with a release. POST /sources for a signed S3 pair, upload a small tarball containing a Procfile with both web: and release: processes (release script prints lines over ~10s so the streaming path is observable), then POST /apps/eb-sdk-test-staging/builds with the get_url. Wait for status succeeded.
  6. Promote and observe the release-command output stream live to stdout:
    ./bin/run pipelines:promote -a eb-sdk-test-staging
    
    Expected output: Running release command... followed by the streamed step 1 / 10 ... step 10 / 10 lines emitted in real time, then Promotion successful and eb-sdk-test-prod: succeeded. This exercises HerokuApiClient.stream() → SDK onReleaseStreamstream.pipeTo(WritableStream)process.stdout.
  7. Verify pipelines:destroy removes the pipeline:
    ./bin/run pipelines:destroy eb-sdk-test-pipeline
    ./bin/run pipelines                              # the pipeline is gone
    
  8. Cleanup: heroku apps:destroy for both apps.

Related Issues

GUS work item: W-22265744

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.
@eablack eablack requested a review from a team as a code owner May 18, 2026 23:25
…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.
Copy link
Copy Markdown
Contributor

@tlowrimore-heroku tlowrimore-heroku left a comment

Choose a reason for hiding this comment

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

LGTM!

@eablack eablack merged commit b15f593 into feat/heroku-sdk-integration May 20, 2026
5 of 18 checks passed
@eablack eablack deleted the eb/refactor/sdk-pipelines-commands branch May 20, 2026 17:07
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.
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.

2 participants