@heroku/sdk 0.6.2 → 0.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/resources/data/database.d.ts +12 -39
- package/dist/resources/data/database.js +8 -12
- package/dist/resources/data/postgres-database.d.ts +1 -11
- package/dist/resources/data/postgres-database.js +3 -7
- package/dist/resources/data/transfer.d.ts +3 -43
- package/dist/resources/data/transfer.js +3 -7
- package/dist/resources/extensions/platform.d.ts +1 -1
- package/dist/resources/extensions/platform.js +1 -1
- package/dist/resources/platform/dyno/exec.d.ts +134 -0
- package/dist/resources/platform/dyno/exec.js +187 -0
- package/dist/resources/platform/dyno/index.d.ts +13 -0
- package/dist/resources/platform/dyno/index.js +13 -0
- package/dist/resources/platform/dyno/list-extended.d.ts +50 -0
- package/dist/resources/platform/dyno/list-extended.js +28 -0
- package/dist/resources/platform/dyno/wait-for-release.d.ts +76 -0
- package/dist/resources/platform/dyno/wait-for-release.js +83 -0
- package/package.json +2 -2
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
import type { DatabaseInfoResult } from '@heroku/types/data';
|
|
1
|
+
import type { DatabaseCancelUpgradeResult, DatabaseDryRunUpgradeOpts, DatabaseDryRunUpgradeResult, DatabaseInfoResult, DatabasePrepareUpgradeOpts, DatabasePrepareUpgradeResult, DatabaseRunUpgradeOpts, DatabaseRunUpgradeResult, DatabaseUpgradeWaitStatusResult } from '@heroku/types/data';
|
|
2
2
|
import type { ResourceCtx } from '../../core/extend-resource.js';
|
|
3
3
|
export type DatabaseOptions = {
|
|
4
4
|
signal?: AbortSignal;
|
|
5
5
|
};
|
|
6
|
-
export type DatabaseUpgradeBody = {
|
|
7
|
-
version?: string;
|
|
8
|
-
};
|
|
9
6
|
export type DatabaseDescribeResult = DatabaseInfoResult & {
|
|
10
7
|
following?: string;
|
|
11
8
|
'hot_standby?'?: boolean;
|
|
@@ -14,41 +11,17 @@ export type DatabaseDescribeResult = DatabaseInfoResult & {
|
|
|
14
11
|
postgres_version?: string;
|
|
15
12
|
'standalone?'?: boolean;
|
|
16
13
|
};
|
|
17
|
-
export
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
export type DatabaseUpgradeResponse = {
|
|
24
|
-
message: string;
|
|
25
|
-
};
|
|
26
|
-
type DatabaseDataClient = {
|
|
27
|
-
database: {
|
|
28
|
-
cancelUpgrade(name: string): Promise<DatabaseUpgradeResponse>;
|
|
29
|
-
dryRunUpgrade(name: string, body: DatabaseUpgradeBody): Promise<DatabaseUpgradeResponse>;
|
|
30
|
-
info(name: string): Promise<DatabaseDescribeResult>;
|
|
31
|
-
prepareUpgrade(name: string, body: DatabaseUpgradeBody): Promise<DatabaseUpgradeResponse>;
|
|
32
|
-
runUpgrade(name: string, body: DatabaseUpgradeBody): Promise<DatabaseUpgradeResponse>;
|
|
33
|
-
upgradeWaitStatus(name: string): Promise<DatabaseUpgradeWaitResult>;
|
|
34
|
-
};
|
|
35
|
-
};
|
|
36
|
-
type DatabaseCtx = {
|
|
37
|
-
data: DatabaseDataClient;
|
|
38
|
-
platform: ResourceCtx['platform'];
|
|
39
|
-
};
|
|
40
|
-
export declare function describe(ctx: DatabaseCtx, appIdentity: string, addonIdentity?: string, options?: DatabaseOptions): Promise<DatabaseDescribeResult>;
|
|
41
|
-
export declare function upgradeWaitStatus(ctx: DatabaseCtx, appIdentity: string, addonIdentity?: string, options?: DatabaseOptions): Promise<DatabaseUpgradeWaitResult>;
|
|
42
|
-
export declare function dryRunUpgrade(ctx: DatabaseCtx, appIdentity: string, addonIdentity?: string, body?: DatabaseUpgradeBody, options?: DatabaseOptions): Promise<DatabaseUpgradeResponse>;
|
|
43
|
-
export declare function runUpgrade(ctx: DatabaseCtx, appIdentity: string, addonIdentity?: string, body?: DatabaseUpgradeBody, options?: DatabaseOptions): Promise<DatabaseUpgradeResponse>;
|
|
44
|
-
export declare function prepareUpgrade(ctx: DatabaseCtx, appIdentity: string, addonIdentity?: string, body?: DatabaseUpgradeBody, options?: DatabaseOptions): Promise<DatabaseUpgradeResponse>;
|
|
45
|
-
export declare function cancelUpgrade(ctx: DatabaseCtx, appIdentity: string, addonIdentity?: string, options?: DatabaseOptions): Promise<DatabaseUpgradeResponse>;
|
|
14
|
+
export declare function describe(ctx: Pick<ResourceCtx, 'data' | 'platform'>, appIdentity: string, addonIdentity?: string, options?: DatabaseOptions): Promise<DatabaseDescribeResult>;
|
|
15
|
+
export declare function upgradeWaitStatus(ctx: Pick<ResourceCtx, 'data' | 'platform'>, appIdentity: string, addonIdentity?: string, options?: DatabaseOptions): Promise<DatabaseUpgradeWaitStatusResult>;
|
|
16
|
+
export declare function dryRunUpgrade(ctx: Pick<ResourceCtx, 'data' | 'platform'>, appIdentity: string, addonIdentity?: string, body?: DatabaseDryRunUpgradeOpts, options?: DatabaseOptions): Promise<DatabaseDryRunUpgradeResult>;
|
|
17
|
+
export declare function runUpgrade(ctx: Pick<ResourceCtx, 'data' | 'platform'>, appIdentity: string, addonIdentity?: string, body?: DatabaseRunUpgradeOpts, options?: DatabaseOptions): Promise<DatabaseRunUpgradeResult>;
|
|
18
|
+
export declare function prepareUpgrade(ctx: Pick<ResourceCtx, 'data' | 'platform'>, appIdentity: string, addonIdentity?: string, body?: DatabasePrepareUpgradeOpts, options?: DatabaseOptions): Promise<DatabasePrepareUpgradeResult>;
|
|
19
|
+
export declare function cancelUpgrade(ctx: Pick<ResourceCtx, 'data' | 'platform'>, appIdentity: string, addonIdentity?: string, options?: DatabaseOptions): Promise<DatabaseCancelUpgradeResult>;
|
|
46
20
|
export declare const databaseExtensions: import("../../core/extend-resource.js").ResourceExtension<"data", "database", {
|
|
47
|
-
cancelUpgrade: (appIdentity: string, addonIdentity?: string, options?: DatabaseOptions) => Promise<
|
|
21
|
+
cancelUpgrade: (appIdentity: string, addonIdentity?: string, options?: DatabaseOptions) => Promise<DatabaseCancelUpgradeResult>;
|
|
48
22
|
describe: (appIdentity: string, addonIdentity?: string, options?: DatabaseOptions) => Promise<DatabaseDescribeResult>;
|
|
49
|
-
dryRunUpgrade: (appIdentity: string, addonIdentity?: string, body?:
|
|
50
|
-
prepareUpgrade: (appIdentity: string, addonIdentity?: string, body?:
|
|
51
|
-
runUpgrade: (appIdentity: string, addonIdentity?: string, body?:
|
|
52
|
-
upgradeWaitStatus: (appIdentity: string, addonIdentity?: string, options?: DatabaseOptions) => Promise<
|
|
23
|
+
dryRunUpgrade: (appIdentity: string, addonIdentity?: string, body?: DatabaseDryRunUpgradeOpts, options?: DatabaseOptions) => Promise<DatabaseDryRunUpgradeResult>;
|
|
24
|
+
prepareUpgrade: (appIdentity: string, addonIdentity?: string, body?: DatabasePrepareUpgradeOpts, options?: DatabaseOptions) => Promise<DatabasePrepareUpgradeResult>;
|
|
25
|
+
runUpgrade: (appIdentity: string, addonIdentity?: string, body?: DatabaseRunUpgradeOpts, options?: DatabaseOptions) => Promise<DatabaseRunUpgradeResult>;
|
|
26
|
+
upgradeWaitStatus: (appIdentity: string, addonIdentity?: string, options?: DatabaseOptions) => Promise<DatabaseUpgradeWaitStatusResult>;
|
|
53
27
|
}>;
|
|
54
|
-
export {};
|
|
@@ -30,15 +30,11 @@ export async function cancelUpgrade(ctx, appIdentity, addonIdentity, options = {
|
|
|
30
30
|
const addon = await resolvePgDatabase(ctx, { appIdentity, input: addonIdentity, ...options });
|
|
31
31
|
return ctx.data.database.cancelUpgrade(addon.id);
|
|
32
32
|
}
|
|
33
|
-
export const databaseExtensions = extendResource('data', 'database',
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
runUpgrade: (appIdentity, addonIdentity, body, options) => runUpgrade(ctx, appIdentity, addonIdentity, body, options),
|
|
42
|
-
upgradeWaitStatus: (appIdentity, addonIdentity, options) => upgradeWaitStatus(ctx, appIdentity, addonIdentity, options),
|
|
43
|
-
};
|
|
44
|
-
});
|
|
33
|
+
export const databaseExtensions = extendResource('data', 'database', ctx => ({
|
|
34
|
+
cancelUpgrade: (appIdentity, addonIdentity, options) => cancelUpgrade(ctx, appIdentity, addonIdentity, options),
|
|
35
|
+
describe: (appIdentity, addonIdentity, options) => describe(ctx, appIdentity, addonIdentity, options),
|
|
36
|
+
dryRunUpgrade: (appIdentity, addonIdentity, body, options) => dryRunUpgrade(ctx, appIdentity, addonIdentity, body, options),
|
|
37
|
+
prepareUpgrade: (appIdentity, addonIdentity, body, options) => prepareUpgrade(ctx, appIdentity, addonIdentity, body, options),
|
|
38
|
+
runUpgrade: (appIdentity, addonIdentity, body, options) => runUpgrade(ctx, appIdentity, addonIdentity, body, options),
|
|
39
|
+
upgradeWaitStatus: (appIdentity, addonIdentity, options) => upgradeWaitStatus(ctx, appIdentity, addonIdentity, options),
|
|
40
|
+
}));
|
|
@@ -16,17 +16,7 @@ export type CredentialInfo = {
|
|
|
16
16
|
state: string;
|
|
17
17
|
uuid: string;
|
|
18
18
|
};
|
|
19
|
-
|
|
20
|
-
postgresDatabase: {
|
|
21
|
-
listCredentials(name: string): Promise<CredentialInfo[]>;
|
|
22
|
-
};
|
|
23
|
-
};
|
|
24
|
-
type CredentialsCtx = {
|
|
25
|
-
data: CredentialsDataClient;
|
|
26
|
-
platform: ResourceCtx['platform'];
|
|
27
|
-
};
|
|
28
|
-
export declare function listCredentials(ctx: CredentialsCtx, appIdentity: string, addonIdentity?: string, options?: ListCredentialsOptions): Promise<CredentialInfo[]>;
|
|
19
|
+
export declare function listCredentials(ctx: Pick<ResourceCtx, 'data' | 'platform'>, appIdentity: string, addonIdentity?: string, options?: ListCredentialsOptions): Promise<CredentialInfo[]>;
|
|
29
20
|
export declare const postgresDatabaseExtensions: import("../../core/extend-resource.js").ResourceExtension<"data", "postgresDatabase", {
|
|
30
21
|
listCredentials: (appIdentity: string, addonIdentity?: string, options?: ListCredentialsOptions) => Promise<CredentialInfo[]>;
|
|
31
22
|
}>;
|
|
32
|
-
export {};
|
|
@@ -5,10 +5,6 @@ export async function listCredentials(ctx, appIdentity, addonIdentity, options =
|
|
|
5
5
|
const addon = await resolvePgDatabase(ctx, { appIdentity, input: addonIdentity, ...options });
|
|
6
6
|
return ctx.data.postgresDatabase.listCredentials(addon.id);
|
|
7
7
|
}
|
|
8
|
-
export const postgresDatabaseExtensions = extendResource('data', 'postgresDatabase',
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
return {
|
|
12
|
-
listCredentials: (appIdentity, addonIdentity, options) => listCredentials(ctx, appIdentity, addonIdentity, options),
|
|
13
|
-
};
|
|
14
|
-
});
|
|
8
|
+
export const postgresDatabaseExtensions = extendResource('data', 'postgresDatabase', ctx => ({
|
|
9
|
+
listCredentials: (appIdentity, addonIdentity, options) => listCredentials(ctx, appIdentity, addonIdentity, options),
|
|
10
|
+
}));
|
|
@@ -1,49 +1,9 @@
|
|
|
1
|
+
import type { TransferListByAppResult } from '@heroku/types/data';
|
|
1
2
|
import type { ResourceCtx } from '../../core/extend-resource.js';
|
|
2
3
|
export type TransferOptions = {
|
|
3
4
|
signal?: AbortSignal;
|
|
4
5
|
};
|
|
5
|
-
export
|
|
6
|
-
canceled_at: string;
|
|
7
|
-
created_at: string;
|
|
8
|
-
deleted_at: string;
|
|
9
|
-
finished_at: string;
|
|
10
|
-
from_name: string;
|
|
11
|
-
from_type: string;
|
|
12
|
-
from_url: string;
|
|
13
|
-
logs: Array<{
|
|
14
|
-
created_at: string;
|
|
15
|
-
level: string;
|
|
16
|
-
message: string;
|
|
17
|
-
}>;
|
|
18
|
-
num: number;
|
|
19
|
-
num_keep: number;
|
|
20
|
-
options: Record<string, unknown>;
|
|
21
|
-
processed_bytes: number;
|
|
22
|
-
purged_at: string;
|
|
23
|
-
schedule?: {
|
|
24
|
-
uuid: string;
|
|
25
|
-
};
|
|
26
|
-
source_bytes: number;
|
|
27
|
-
started_at: string;
|
|
28
|
-
succeeded: boolean;
|
|
29
|
-
to_name: string;
|
|
30
|
-
to_type: string;
|
|
31
|
-
to_url: string;
|
|
32
|
-
updated_at: string;
|
|
33
|
-
uuid: string;
|
|
34
|
-
warnings: number;
|
|
35
|
-
};
|
|
36
|
-
type TransferDataClient = {
|
|
37
|
-
transfer: {
|
|
38
|
-
listByApp(name: string): Promise<BackupTransfer[]>;
|
|
39
|
-
};
|
|
40
|
-
};
|
|
41
|
-
type TransferCtx = {
|
|
42
|
-
data: TransferDataClient;
|
|
43
|
-
platform: ResourceCtx['platform'];
|
|
44
|
-
};
|
|
45
|
-
export declare function listByApp(ctx: TransferCtx, appIdentity: string, addonIdentity?: string, options?: TransferOptions): Promise<BackupTransfer[]>;
|
|
6
|
+
export declare function listByApp(ctx: Pick<ResourceCtx, 'data' | 'platform'>, appIdentity: string, addonIdentity?: string, options?: TransferOptions): Promise<TransferListByAppResult>;
|
|
46
7
|
export declare const transferExtensions: import("../../core/extend-resource.js").ResourceExtension<"data", "transfer", {
|
|
47
|
-
listByApp: (appIdentity: string, addonIdentity?: string, options?: TransferOptions) => Promise<
|
|
8
|
+
listByApp: (appIdentity: string, addonIdentity?: string, options?: TransferOptions) => Promise<TransferListByAppResult>;
|
|
48
9
|
}>;
|
|
49
|
-
export {};
|
|
@@ -14,10 +14,6 @@ export async function listByApp(ctx, appIdentity, addonIdentity, options = {}) {
|
|
|
14
14
|
}
|
|
15
15
|
return ctx.data.transfer.listByApp(addon.id);
|
|
16
16
|
}
|
|
17
|
-
export const transferExtensions = extendResource('data', 'transfer',
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return {
|
|
21
|
-
listByApp: (appIdentity, addonIdentity, options) => listByApp(ctx, appIdentity, addonIdentity, options),
|
|
22
|
-
};
|
|
23
|
-
});
|
|
17
|
+
export const transferExtensions = extendResource('data', 'transfer', ctx => ({
|
|
18
|
+
listByApp: (appIdentity, addonIdentity, options) => listByApp(ctx, appIdentity, addonIdentity, options),
|
|
19
|
+
}));
|
|
@@ -2,7 +2,7 @@ export { addOnExtensions } from '../platform/add-on/index.js';
|
|
|
2
2
|
export { appExtensions, type AppInfo, type DiffAppsOptions, type DiffRow, type PipelineCouplingDetail, } from '../platform/app/index.js';
|
|
3
3
|
export { containerExtensions, type DockerReleasesBatchUpdateOpts, NotAContainerAppError, type ReleaseDockerImagesResult, type RemoveProcessTypesOpts, } from '../platform/container/index.js';
|
|
4
4
|
export { type CreateAndWaitOptions, domainExtensions, type DomainOptions, type WaitForReadyOptions, } from '../platform/domain/index.js';
|
|
5
|
-
export { dynoExtensions, DynoNotReadyError, type DynoState, runDyno, type RunDynoOptions, waitForInfo, type WaitForInfoOptions, } from '../platform/dyno/index.js';
|
|
5
|
+
export { DynoCrashedError, type DynoExtended, type DynoExtendedFields, dynoExtensions, DynoNotReadyError, type DynoState, enableExec, exchangeExecCredentials, type ExchangeExecCredentialsOptions, type ExecCredentials, type ExecOptions, type ExecPrereqs, execPrereqs, type ExecReservation, execStatus, type ExecStatusOptions, listExtended, type ListExtendedOptions, restartForExec, type RestartForExecOptions, runDyno, type RunDynoOptions, waitForInfo, type WaitForInfoOptions, waitForRelease, type WaitForReleaseOptions, type WaitForReleaseProgress, type WaitForReleaseResult, } from '../platform/dyno/index.js';
|
|
6
6
|
export { type AttachmentAttachedEvent, type AttachmentDetachedEvent, type AttachmentUpdatedEvent, type HerokuLogEvent, logSessionExtensions, parseHerokuLogLine, type ProvisioningCompletedEvent, type ScaledToEntry, type ScaledToEvent, type StartingProcessEvent, type StateChangedEvent, } from '../platform/log-session/index.js';
|
|
7
7
|
export { pipelineCouplingExtensions } from '../platform/pipeline/coupling.js';
|
|
8
8
|
export { pipelineExtensions } from '../platform/pipeline/index.js';
|
|
@@ -2,7 +2,7 @@ export { addOnExtensions } from '../platform/add-on/index.js';
|
|
|
2
2
|
export { appExtensions, } from '../platform/app/index.js';
|
|
3
3
|
export { containerExtensions, NotAContainerAppError, } from '../platform/container/index.js';
|
|
4
4
|
export { domainExtensions, } from '../platform/domain/index.js';
|
|
5
|
-
export { dynoExtensions, DynoNotReadyError, runDyno, waitForInfo, } from '../platform/dyno/index.js';
|
|
5
|
+
export { DynoCrashedError, dynoExtensions, DynoNotReadyError, enableExec, exchangeExecCredentials, execPrereqs, execStatus, listExtended, restartForExec, runDyno, waitForInfo, waitForRelease, } from '../platform/dyno/index.js';
|
|
6
6
|
export { logSessionExtensions, parseHerokuLogLine, } from '../platform/log-session/index.js';
|
|
7
7
|
export { pipelineCouplingExtensions } from '../platform/pipeline/coupling.js';
|
|
8
8
|
export { pipelineExtensions } from '../platform/pipeline/index.js';
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { HerokuApiClientOptions } from '@heroku/heroku-fetch';
|
|
2
|
+
import type { App, BuildpackInstallation, ConfigVar, Dyno } from '@heroku/types/3.sdk';
|
|
3
|
+
import type { ResourceCtx } from '../../../core/extend-resource.js';
|
|
4
|
+
import { type WaitForInfoOptions } from './wait-for-info.js';
|
|
5
|
+
export type ExecOptions = {
|
|
6
|
+
signal?: AbortSignal;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Raw facts the CLI needs to decide whether — and how — Heroku Exec can
|
|
10
|
+
* be enabled for an app. This helper only gathers; every branch
|
|
11
|
+
* (Shield restriction, container-stack warning, missing-buildpack
|
|
12
|
+
* error, the deprecated `HEROKU_EXEC_URL` add-on notice) stays in the
|
|
13
|
+
* caller so the SDK owns no presentation or prompting.
|
|
14
|
+
*/
|
|
15
|
+
export type ExecPrereqs = {
|
|
16
|
+
/** Installed buildpacks, for the exec-buildpack presence check. */
|
|
17
|
+
buildpacks: BuildpackInstallation[];
|
|
18
|
+
/** `build_stack.name`, e.g. `'container'`. */
|
|
19
|
+
buildStack: string | undefined;
|
|
20
|
+
/** Full config-var map; the caller reads `HEROKU_EXEC_URL` off it. */
|
|
21
|
+
configVars: ConfigVar;
|
|
22
|
+
/** Whether the `runtime-heroku-exec` feature is already enabled. */
|
|
23
|
+
featureEnabled: boolean;
|
|
24
|
+
/** App generation; `'fir'` apps do not support exec. */
|
|
25
|
+
generation: string;
|
|
26
|
+
/** App's Private Space, if any (carries `shield`). */
|
|
27
|
+
space: App['space'];
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Gather the app facts that gate Heroku Exec in one parallel round-trip:
|
|
31
|
+
* app info, buildpack installations, config vars, and the exec feature.
|
|
32
|
+
*
|
|
33
|
+
* Returns raw facts only — the caller branches on them. `configVars` is
|
|
34
|
+
* returned whole because the caller inspects `HEROKU_EXEC_URL` (the
|
|
35
|
+
* deprecated add-on) as well as passing config through to the exchange.
|
|
36
|
+
*/
|
|
37
|
+
export declare function execPrereqs(ctx: Pick<ResourceCtx, 'platform'>, appIdentity: string, options?: ExecOptions): Promise<ExecPrereqs>;
|
|
38
|
+
/**
|
|
39
|
+
* Enable the `runtime-heroku-exec` feature on an app. Used both on the
|
|
40
|
+
* first-run restart path and on the Private-Space buildpack path, so it
|
|
41
|
+
* stays a bare enable — the caller decides whether a restart follows.
|
|
42
|
+
*/
|
|
43
|
+
export declare function enableExec(ctx: Pick<ResourceCtx, 'platform'>, appIdentity: string, options?: ExecOptions): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Thrown by {@link restartForExec} the moment the awaited dyno reports
|
|
46
|
+
* `crashed`, so the caller can surface the failure immediately rather
|
|
47
|
+
* than waiting out the remaining poll attempts.
|
|
48
|
+
*/
|
|
49
|
+
export declare class DynoCrashedError extends Error {
|
|
50
|
+
readonly dyno: Dyno;
|
|
51
|
+
readonly id = "dyno_crashed";
|
|
52
|
+
constructor(dyno: Dyno);
|
|
53
|
+
}
|
|
54
|
+
export type RestartForExecOptions = WaitForInfoOptions & {
|
|
55
|
+
/**
|
|
56
|
+
* Milliseconds to wait after enabling the feature before restarting,
|
|
57
|
+
* letting the enable propagate. Defaults to 2000, matching the
|
|
58
|
+
* historical CLI behavior.
|
|
59
|
+
*/
|
|
60
|
+
settleMs?: number;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Restart all of an app's dynos and wait for the named dyno to come
|
|
64
|
+
* back `up`. Enabling Heroku Exec for the first time requires a dyno
|
|
65
|
+
* restart, so this bundles the settle delay, `dyno.restartAll`, and the
|
|
66
|
+
* wait-for-`up` poll into one step. A `crashed` dyno throws
|
|
67
|
+
* {@link DynoCrashedError} at once instead of exhausting the attempts.
|
|
68
|
+
*
|
|
69
|
+
* The wait is bounded by {@link WaitForInfoOptions.attempts} (default
|
|
70
|
+
* 20). Callers that need to tolerate a slow boot should raise it.
|
|
71
|
+
*/
|
|
72
|
+
export declare function restartForExec(ctx: Pick<ResourceCtx, 'platform'>, appIdentity: string, dynoIdentity: string, options?: RestartForExecOptions): Promise<Dyno>;
|
|
73
|
+
/** Connection info the exec-manager returns for a client-key exchange. */
|
|
74
|
+
export type ExecCredentials = {
|
|
75
|
+
client_user: string;
|
|
76
|
+
dyno_ip: string;
|
|
77
|
+
proxy_public_key: string;
|
|
78
|
+
tunnel_host: string;
|
|
79
|
+
};
|
|
80
|
+
export type ExchangeExecCredentialsOptions = {
|
|
81
|
+
/** API key used as the Basic-auth password (username is the app). */
|
|
82
|
+
apiKey: string;
|
|
83
|
+
/**
|
|
84
|
+
* Options forwarded to the raw `HerokuApiClient`. The exec-manager is
|
|
85
|
+
* not in the routes registry, so the call is issued via a bare client
|
|
86
|
+
* with `service: 'custom'`.
|
|
87
|
+
*/
|
|
88
|
+
clientOptions?: Omit<HerokuApiClientOptions, 'baseUrl' | 'service' | 'token'>;
|
|
89
|
+
/**
|
|
90
|
+
* App config vars. When `HEROKU_EXEC_URL` is present (the deprecated
|
|
91
|
+
* add-on), its embedded credentials and the `/api/v1` prefix are used.
|
|
92
|
+
*/
|
|
93
|
+
configVars?: ConfigVar;
|
|
94
|
+
/** Target dyno, e.g. `'web.1'`. */
|
|
95
|
+
dyno: string;
|
|
96
|
+
/** Extra request headers (e.g. from `HEROKU_HEADERS`). */
|
|
97
|
+
headers?: Record<string, string>;
|
|
98
|
+
/** OpenSSH-format public key generated by the caller. */
|
|
99
|
+
publicKey: string;
|
|
100
|
+
signal?: AbortSignal;
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* Exchange a client public key for exec connection info.
|
|
104
|
+
*
|
|
105
|
+
* Issues `PUT {execHost}{apiPath}/{dyno}` with `{client_key}` and Basic
|
|
106
|
+
* auth (`app:apiKey`, or the credentials embedded in `HEROKU_EXEC_URL`)
|
|
107
|
+
* via a bare `HerokuApiClient` — the exec-manager is a custom host with
|
|
108
|
+
* no routes entry and no bearer token (the empty `token` lets the Basic
|
|
109
|
+
* `Authorization` header survive). The caller feeds the returned info,
|
|
110
|
+
* plus the private key it holds, to its own SSH transport; the SDK does
|
|
111
|
+
* not open the connection.
|
|
112
|
+
*/
|
|
113
|
+
export declare function exchangeExecCredentials(appIdentity: string, options: ExchangeExecCredentialsOptions): Promise<ExecCredentials>;
|
|
114
|
+
/** A running exec reservation, as reported by the exec-manager status endpoint. */
|
|
115
|
+
export type ExecReservation = {
|
|
116
|
+
[key: string]: unknown;
|
|
117
|
+
dyno_name: string;
|
|
118
|
+
};
|
|
119
|
+
export type ExecStatusOptions = {
|
|
120
|
+
apiKey: string;
|
|
121
|
+
clientOptions?: Omit<HerokuApiClientOptions, 'baseUrl' | 'service' | 'token'>;
|
|
122
|
+
configVars?: ConfigVar;
|
|
123
|
+
headers?: Record<string, string>;
|
|
124
|
+
signal?: AbortSignal;
|
|
125
|
+
};
|
|
126
|
+
/**
|
|
127
|
+
* List the app's running exec reservations.
|
|
128
|
+
*
|
|
129
|
+
* Issues `GET {execHost}{apiPath}` (no dyno segment) with the same
|
|
130
|
+
* custom-host Basic-auth setup as {@link exchangeExecCredentials}. The
|
|
131
|
+
* caller correlates each reservation's `dyno_name` with live dyno state
|
|
132
|
+
* for display.
|
|
133
|
+
*/
|
|
134
|
+
export declare function execStatus(appIdentity: string, options: ExecStatusOptions): Promise<ExecReservation[]>;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { HerokuApiClient, NotFoundError } from '@heroku/heroku-fetch';
|
|
2
|
+
import { wait } from '../../../utils/wait.js';
|
|
3
|
+
import { waitForInfo } from './wait-for-info.js';
|
|
4
|
+
/** The app feature that gates Heroku Exec. */
|
|
5
|
+
const EXEC_FEATURE = 'runtime-heroku-exec';
|
|
6
|
+
/** Default exec-manager host used when no override is configured. */
|
|
7
|
+
const DEFAULT_EXEC_HOST = 'https://exec-manager.heroku.com/';
|
|
8
|
+
/** Exec-manager API prefixes. v1 is the legacy add-on path; v2 is current. */
|
|
9
|
+
const EXEC_API_PATH_LEGACY = '/api/v1';
|
|
10
|
+
const EXEC_API_PATH_CURRENT = '/api/v2';
|
|
11
|
+
/** Milliseconds to let the feature-enable settle before restarting dynos. */
|
|
12
|
+
const DEFAULT_SETTLE_MS = 2000;
|
|
13
|
+
/**
|
|
14
|
+
* Gather the app facts that gate Heroku Exec in one parallel round-trip:
|
|
15
|
+
* app info, buildpack installations, config vars, and the exec feature.
|
|
16
|
+
*
|
|
17
|
+
* Returns raw facts only — the caller branches on them. `configVars` is
|
|
18
|
+
* returned whole because the caller inspects `HEROKU_EXEC_URL` (the
|
|
19
|
+
* deprecated add-on) as well as passing config through to the exchange.
|
|
20
|
+
*/
|
|
21
|
+
export async function execPrereqs(ctx, appIdentity, options = {}) {
|
|
22
|
+
options.signal?.throwIfAborted();
|
|
23
|
+
const platform = options.signal ? ctx.platform.withOptions({ signal: options.signal }) : ctx.platform;
|
|
24
|
+
const [app, buildpacks, configVars, feature] = await Promise.all([
|
|
25
|
+
platform.app.info(appIdentity),
|
|
26
|
+
platform.buildpackInstallation.list(appIdentity),
|
|
27
|
+
platform.configVar.infoForApp(appIdentity),
|
|
28
|
+
// fir apps have no runtime-heroku-exec feature, so this 404s there. A
|
|
29
|
+
// missing feature must not sink the whole gather — the caller reads
|
|
30
|
+
// `generation` to reject fir. Only a 404 degrades to disabled; other
|
|
31
|
+
// errors still propagate.
|
|
32
|
+
platform.appFeature.info(appIdentity, EXEC_FEATURE).catch((error) => {
|
|
33
|
+
if (error instanceof NotFoundError)
|
|
34
|
+
return { enabled: false };
|
|
35
|
+
throw error;
|
|
36
|
+
}),
|
|
37
|
+
]);
|
|
38
|
+
return {
|
|
39
|
+
buildpacks,
|
|
40
|
+
buildStack: app.build_stack?.name,
|
|
41
|
+
configVars,
|
|
42
|
+
featureEnabled: Boolean(feature.enabled),
|
|
43
|
+
generation: app.generation,
|
|
44
|
+
space: app.space,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Enable the `runtime-heroku-exec` feature on an app. Used both on the
|
|
49
|
+
* first-run restart path and on the Private-Space buildpack path, so it
|
|
50
|
+
* stays a bare enable — the caller decides whether a restart follows.
|
|
51
|
+
*/
|
|
52
|
+
export async function enableExec(ctx, appIdentity, options = {}) {
|
|
53
|
+
options.signal?.throwIfAborted();
|
|
54
|
+
const platform = options.signal ? ctx.platform.withOptions({ signal: options.signal }) : ctx.platform;
|
|
55
|
+
await platform.appFeature.update(appIdentity, EXEC_FEATURE, { enabled: true });
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Thrown by {@link restartForExec} the moment the awaited dyno reports
|
|
59
|
+
* `crashed`, so the caller can surface the failure immediately rather
|
|
60
|
+
* than waiting out the remaining poll attempts.
|
|
61
|
+
*/
|
|
62
|
+
export class DynoCrashedError extends Error {
|
|
63
|
+
dyno;
|
|
64
|
+
id = 'dyno_crashed';
|
|
65
|
+
constructor(dyno) {
|
|
66
|
+
super(`Dyno '${dyno.name}' crashed while waiting for Heroku Exec to start.`);
|
|
67
|
+
this.dyno = dyno;
|
|
68
|
+
this.name = 'DynoCrashedError';
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Restart all of an app's dynos and wait for the named dyno to come
|
|
73
|
+
* back `up`. Enabling Heroku Exec for the first time requires a dyno
|
|
74
|
+
* restart, so this bundles the settle delay, `dyno.restartAll`, and the
|
|
75
|
+
* wait-for-`up` poll into one step. A `crashed` dyno throws
|
|
76
|
+
* {@link DynoCrashedError} at once instead of exhausting the attempts.
|
|
77
|
+
*
|
|
78
|
+
* The wait is bounded by {@link WaitForInfoOptions.attempts} (default
|
|
79
|
+
* 20). Callers that need to tolerate a slow boot should raise it.
|
|
80
|
+
*/
|
|
81
|
+
export async function restartForExec(ctx, appIdentity, dynoIdentity, options = {}) {
|
|
82
|
+
const { settleMs = DEFAULT_SETTLE_MS, ...waitOptions } = options;
|
|
83
|
+
const { signal } = waitOptions;
|
|
84
|
+
signal?.throwIfAborted();
|
|
85
|
+
const platform = signal ? ctx.platform.withOptions({ signal }) : ctx.platform;
|
|
86
|
+
await wait(settleMs, signal);
|
|
87
|
+
await platform.dyno.restartAll(appIdentity);
|
|
88
|
+
return waitForInfo(ctx, appIdentity, dynoIdentity, {
|
|
89
|
+
...waitOptions,
|
|
90
|
+
onPoll(dyno) {
|
|
91
|
+
if (dyno.state === 'crashed') {
|
|
92
|
+
throw new DynoCrashedError(dyno);
|
|
93
|
+
}
|
|
94
|
+
waitOptions.onPoll?.(dyno);
|
|
95
|
+
},
|
|
96
|
+
states: ['up'],
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Exchange a client public key for exec connection info.
|
|
101
|
+
*
|
|
102
|
+
* Issues `PUT {execHost}{apiPath}/{dyno}` with `{client_key}` and Basic
|
|
103
|
+
* auth (`app:apiKey`, or the credentials embedded in `HEROKU_EXEC_URL`)
|
|
104
|
+
* via a bare `HerokuApiClient` — the exec-manager is a custom host with
|
|
105
|
+
* no routes entry and no bearer token (the empty `token` lets the Basic
|
|
106
|
+
* `Authorization` header survive). The caller feeds the returned info,
|
|
107
|
+
* plus the private key it holds, to its own SSH transport; the SDK does
|
|
108
|
+
* not open the connection.
|
|
109
|
+
*/
|
|
110
|
+
export async function exchangeExecCredentials(appIdentity, options) {
|
|
111
|
+
options.signal?.throwIfAborted();
|
|
112
|
+
const endpoint = resolveExecEndpoint(appIdentity, options.apiKey, options.configVars);
|
|
113
|
+
const client = newExecClient(endpoint.baseUrl, options.clientOptions);
|
|
114
|
+
const path = `${endpoint.apiPath}/${encodeURIComponent(options.dyno)}`;
|
|
115
|
+
const response = await client.put(path, { client_key: options.publicKey }, {
|
|
116
|
+
headers: {
|
|
117
|
+
...options.headers,
|
|
118
|
+
Authorization: endpoint.authorization,
|
|
119
|
+
'Content-Type': 'application/json',
|
|
120
|
+
},
|
|
121
|
+
signal: options.signal,
|
|
122
|
+
});
|
|
123
|
+
return parseExecBody(response);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* List the app's running exec reservations.
|
|
127
|
+
*
|
|
128
|
+
* Issues `GET {execHost}{apiPath}` (no dyno segment) with the same
|
|
129
|
+
* custom-host Basic-auth setup as {@link exchangeExecCredentials}. The
|
|
130
|
+
* caller correlates each reservation's `dyno_name` with live dyno state
|
|
131
|
+
* for display.
|
|
132
|
+
*/
|
|
133
|
+
export async function execStatus(appIdentity, options) {
|
|
134
|
+
options.signal?.throwIfAborted();
|
|
135
|
+
const endpoint = resolveExecEndpoint(appIdentity, options.apiKey, options.configVars);
|
|
136
|
+
const client = newExecClient(endpoint.baseUrl, options.clientOptions);
|
|
137
|
+
const response = await client.get(endpoint.apiPath, {
|
|
138
|
+
headers: { ...options.headers, Authorization: endpoint.authorization },
|
|
139
|
+
signal: options.signal,
|
|
140
|
+
});
|
|
141
|
+
return parseExecBody(response);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Resolve the exec-manager base URL, API prefix, and Basic-auth header.
|
|
145
|
+
*
|
|
146
|
+
* When `HEROKU_EXEC_URL` is set as a config var (the deprecated add-on),
|
|
147
|
+
* that URL carries its own embedded credentials and uses the `/api/v1`
|
|
148
|
+
* prefix. Otherwise the host comes from the `HEROKU_EXEC_URL`
|
|
149
|
+
* environment override or the default exec-manager, authenticated as
|
|
150
|
+
* `app:apiKey` against `/api/v2`.
|
|
151
|
+
*/
|
|
152
|
+
function resolveExecEndpoint(appIdentity, apiKey, configVars) {
|
|
153
|
+
const configUrl = configVars?.HEROKU_EXEC_URL;
|
|
154
|
+
if (configUrl) {
|
|
155
|
+
const url = new URL(configUrl);
|
|
156
|
+
// URL.username/password are percent-encoded; decode before Basic auth so
|
|
157
|
+
// credentials with reserved characters (e.g. `p%40ss`) authenticate.
|
|
158
|
+
return {
|
|
159
|
+
apiPath: EXEC_API_PATH_LEGACY,
|
|
160
|
+
authorization: toBasicAuth(decodeURIComponent(url.username), decodeURIComponent(url.password)),
|
|
161
|
+
baseUrl: `${url.protocol}//${url.host}`,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
const url = new URL(process.env.HEROKU_EXEC_URL ?? DEFAULT_EXEC_HOST);
|
|
165
|
+
return {
|
|
166
|
+
apiPath: EXEC_API_PATH_CURRENT,
|
|
167
|
+
authorization: toBasicAuth(appIdentity, apiKey),
|
|
168
|
+
baseUrl: `${url.protocol}//${url.host}`,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function newExecClient(baseUrl, clientOptions) {
|
|
172
|
+
return new HerokuApiClient({
|
|
173
|
+
...clientOptions, baseUrl, service: 'custom', token: '',
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
function toBasicAuth(username, password) {
|
|
177
|
+
return `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Parse an exec-manager response body, tolerating either a JSON value or
|
|
181
|
+
* a JSON string (double-encoded), so behavior matches the historical
|
|
182
|
+
* raw `JSON.parse` regardless of the response `Content-Type`.
|
|
183
|
+
*/
|
|
184
|
+
async function parseExecBody(response) {
|
|
185
|
+
const body = (await response.json());
|
|
186
|
+
return (typeof body === 'string' ? JSON.parse(body) : body);
|
|
187
|
+
}
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import type { Formation } from '@heroku/types/3.sdk';
|
|
2
2
|
import type { StickyRouteOptions } from '../../../core/create-client.js';
|
|
3
3
|
import type { ResourceCtx } from '../../../core/extend-resource.js';
|
|
4
|
+
import { type ExchangeExecCredentialsOptions, type ExecOptions, type ExecStatusOptions, type RestartForExecOptions } from './exec.js';
|
|
5
|
+
import { type ListExtendedOptions } from './list-extended.js';
|
|
4
6
|
import { type RunDynoOptions } from './run.js';
|
|
5
7
|
import { type WaitForInfoOptions } from './wait-for-info.js';
|
|
8
|
+
import { type WaitForReleaseOptions } from './wait-for-release.js';
|
|
9
|
+
export { DynoCrashedError, enableExec, exchangeExecCredentials, type ExchangeExecCredentialsOptions, type ExecCredentials, type ExecOptions, type ExecPrereqs, execPrereqs, type ExecReservation, execStatus, type ExecStatusOptions, restartForExec, type RestartForExecOptions, } from './exec.js';
|
|
10
|
+
export { type DynoExtended, type DynoExtendedFields, listExtended, type ListExtendedOptions, } from './list-extended.js';
|
|
6
11
|
export { runDyno, type RunDynoOptions } from './run.js';
|
|
7
12
|
export { DynoNotReadyError, type DynoState, waitForInfo, type WaitForInfoOptions, } from './wait-for-info.js';
|
|
13
|
+
export { waitForRelease, type WaitForReleaseOptions, type WaitForReleaseProgress, type WaitForReleaseResult, } from './wait-for-release.js';
|
|
8
14
|
export type DynoOptions = {
|
|
9
15
|
signal?: AbortSignal;
|
|
10
16
|
};
|
|
@@ -40,11 +46,18 @@ export declare function scaleDynos(ctx: {
|
|
|
40
46
|
}, appIdentity: string, updates: ScaleDynosUpdate[], options?: DynoOptions): Promise<Formation[]>;
|
|
41
47
|
export declare function restartDynos(ctx: Pick<ResourceCtx, 'platform'>, appIdentity: string, target?: RestartDynosTarget, options?: DynoOptions): Promise<void>;
|
|
42
48
|
export declare const dynoExtensions: import("../../../core/extend-resource.js").ResourceExtension<"platform", "dyno", {
|
|
49
|
+
enableExec: (appIdentity: string, options?: ExecOptions) => Promise<void>;
|
|
50
|
+
exchangeExecCredentials: (appIdentity: string, options: ExchangeExecCredentialsOptions) => Promise<import("./exec.js").ExecCredentials>;
|
|
51
|
+
execPrereqs: (appIdentity: string, options?: ExecOptions) => Promise<import("./exec.js").ExecPrereqs>;
|
|
52
|
+
execStatus: (appIdentity: string, options: ExecStatusOptions) => Promise<import("./exec.js").ExecReservation[]>;
|
|
53
|
+
listExtended: (appIdentity: string, options?: ListExtendedOptions) => Promise<import("./list-extended.js").DynoExtended[]>;
|
|
43
54
|
restart: (appIdentity: string, target?: RestartDynosTarget, options?: DynoOptions) => Promise<void>;
|
|
55
|
+
restartForExec: (appIdentity: string, dynoIdentity: string, options?: RestartForExecOptions) => Promise<import("@heroku/types/3.sdk").Dyno>;
|
|
44
56
|
run: (appIdentity: string, command: string, options?: RunDynoOptions) => Promise<import("@heroku/types/3.sdk").Dyno>;
|
|
45
57
|
scale: {
|
|
46
58
|
(appIdentity: string, updates: ScaleDynosUpdate, options?: DynoOptions): Promise<Formation>;
|
|
47
59
|
(appIdentity: string, updates: ScaleDynosUpdate[], options?: DynoOptions): Promise<Formation[]>;
|
|
48
60
|
};
|
|
49
61
|
waitForInfo: (appIdentity: string, dynoIdentity: string, options?: WaitForInfoOptions) => Promise<import("@heroku/types/3.sdk").Dyno>;
|
|
62
|
+
waitForRelease: (appIdentity: string, options?: WaitForReleaseOptions) => Promise<import("./wait-for-release.js").WaitForReleaseResult | undefined>;
|
|
50
63
|
}>;
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { extendResource } from '../../../core/extend-resource.js';
|
|
2
|
+
import { enableExec, exchangeExecCredentials, execPrereqs, execStatus, restartForExec, } from './exec.js';
|
|
3
|
+
import { listExtended } from './list-extended.js';
|
|
2
4
|
import { runDyno } from './run.js';
|
|
3
5
|
import { waitForInfo } from './wait-for-info.js';
|
|
6
|
+
import { waitForRelease } from './wait-for-release.js';
|
|
7
|
+
export { DynoCrashedError, enableExec, exchangeExecCredentials, execPrereqs, execStatus, restartForExec, } from './exec.js';
|
|
8
|
+
export { listExtended, } from './list-extended.js';
|
|
4
9
|
export { runDyno } from './run.js';
|
|
5
10
|
export { DynoNotReadyError, waitForInfo, } from './wait-for-info.js';
|
|
11
|
+
export { waitForRelease, } from './wait-for-release.js';
|
|
6
12
|
export async function scaleDynos(ctx, appIdentity, updates, options = {}) {
|
|
7
13
|
options.signal?.throwIfAborted();
|
|
8
14
|
const platform = options.signal ? ctx.platform.withOptions({ signal: options.signal }) : ctx.platform;
|
|
@@ -25,8 +31,15 @@ export async function restartDynos(ctx, appIdentity, target, options = {}) {
|
|
|
25
31
|
await ctx.platform.dyno.restartFormation(appIdentity, target.type);
|
|
26
32
|
}
|
|
27
33
|
export const dynoExtensions = extendResource('platform', 'dyno', ctx => ({
|
|
34
|
+
enableExec: (appIdentity, options) => (enableExec(ctx, appIdentity, options)),
|
|
35
|
+
exchangeExecCredentials: (appIdentity, options) => (exchangeExecCredentials(appIdentity, options)),
|
|
36
|
+
execPrereqs: (appIdentity, options) => (execPrereqs(ctx, appIdentity, options)),
|
|
37
|
+
execStatus: (appIdentity, options) => (execStatus(appIdentity, options)),
|
|
38
|
+
listExtended: (appIdentity, options) => (listExtended(appIdentity, options)),
|
|
28
39
|
restart: (appIdentity, target, options) => (restartDynos(ctx, appIdentity, target, options)),
|
|
40
|
+
restartForExec: (appIdentity, dynoIdentity, options) => (restartForExec(ctx, appIdentity, dynoIdentity, options)),
|
|
29
41
|
run: (appIdentity, command, options) => (runDyno(ctx, appIdentity, command, options)),
|
|
30
42
|
scale: ((appIdentity, updates, options) => scaleDynos(ctx, appIdentity, updates, options)),
|
|
31
43
|
waitForInfo: (appIdentity, dynoIdentity, options) => (waitForInfo(ctx, appIdentity, dynoIdentity, options)),
|
|
44
|
+
waitForRelease: (appIdentity, options) => (waitForRelease(ctx, appIdentity, options)),
|
|
32
45
|
}));
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { HerokuApiClientOptions } from '@heroku/heroku-fetch';
|
|
2
|
+
import type { Dyno } from '@heroku/types/3.sdk';
|
|
3
|
+
/**
|
|
4
|
+
* Operational detail the platform attaches to each dyno only when the
|
|
5
|
+
* `?extended=true` query is used with the `version=3.sdk` Accept
|
|
6
|
+
* variant. Every field is nullable — the platform omits values it
|
|
7
|
+
* cannot resolve for a given dyno (e.g. a `run` dyno has no `route`).
|
|
8
|
+
*/
|
|
9
|
+
export type DynoExtendedFields = {
|
|
10
|
+
az: null | string;
|
|
11
|
+
execution_plane: null | string;
|
|
12
|
+
fleet: null | string;
|
|
13
|
+
instance: null | string;
|
|
14
|
+
ip: null | string;
|
|
15
|
+
port: null | number;
|
|
16
|
+
region: null | string;
|
|
17
|
+
route: null | string;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* A {@link Dyno} plus the optional {@link DynoExtendedFields} block
|
|
21
|
+
* returned by {@link listExtended}. `extended` is absent unless the
|
|
22
|
+
* request was made with sudo privileges against a supporting stack.
|
|
23
|
+
*/
|
|
24
|
+
export type DynoExtended = Dyno & {
|
|
25
|
+
extended?: DynoExtendedFields;
|
|
26
|
+
};
|
|
27
|
+
export type ListExtendedOptions = {
|
|
28
|
+
/**
|
|
29
|
+
* Options forwarded to the raw `HerokuApiClient`. The route registry
|
|
30
|
+
* has no entry for `GET /apps/{app}/dynos?extended=true`, so this
|
|
31
|
+
* call is issued via a bare client (like the exec-inside path in
|
|
32
|
+
* {@link runDyno}).
|
|
33
|
+
*/
|
|
34
|
+
clientOptions?: HerokuApiClientOptions;
|
|
35
|
+
signal?: AbortSignal;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* List an app's dynos with the extended operational detail block.
|
|
39
|
+
*
|
|
40
|
+
* Issues `GET /apps/{app}/dynos?extended=true` with
|
|
41
|
+
* `Accept: version=3.sdk` via a bare `HerokuApiClient`, because the
|
|
42
|
+
* routes registry has no entry for the `extended` query variant. The
|
|
43
|
+
* `extended` block is only populated for callers with sudo
|
|
44
|
+
* privileges; without them the platform returns ordinary dynos and
|
|
45
|
+
* `extended` is absent.
|
|
46
|
+
*
|
|
47
|
+
* Unlike the routes client, this does not auto-paginate — it issues a
|
|
48
|
+
* single GET, matching the historical CLI behavior.
|
|
49
|
+
*/
|
|
50
|
+
export declare function listExtended(appIdentity: string, options?: ListExtendedOptions): Promise<DynoExtended[]>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { HerokuApiClient } from '@heroku/heroku-fetch';
|
|
2
|
+
const ACCEPT_EXTENDED = 'application/vnd.heroku+json; version=3.sdk';
|
|
3
|
+
/**
|
|
4
|
+
* List an app's dynos with the extended operational detail block.
|
|
5
|
+
*
|
|
6
|
+
* Issues `GET /apps/{app}/dynos?extended=true` with
|
|
7
|
+
* `Accept: version=3.sdk` via a bare `HerokuApiClient`, because the
|
|
8
|
+
* routes registry has no entry for the `extended` query variant. The
|
|
9
|
+
* `extended` block is only populated for callers with sudo
|
|
10
|
+
* privileges; without them the platform returns ordinary dynos and
|
|
11
|
+
* `extended` is absent.
|
|
12
|
+
*
|
|
13
|
+
* Unlike the routes client, this does not auto-paginate — it issues a
|
|
14
|
+
* single GET, matching the historical CLI behavior.
|
|
15
|
+
*/
|
|
16
|
+
export async function listExtended(appIdentity, options = {}) {
|
|
17
|
+
options.signal?.throwIfAborted();
|
|
18
|
+
const apiClient = new HerokuApiClient({
|
|
19
|
+
...options.clientOptions,
|
|
20
|
+
service: 'platform',
|
|
21
|
+
});
|
|
22
|
+
const path = `/apps/${encodeURIComponent(appIdentity)}/dynos?extended=true`;
|
|
23
|
+
const response = await apiClient.get(path, {
|
|
24
|
+
headers: { Accept: ACCEPT_EXTENDED },
|
|
25
|
+
signal: options.signal,
|
|
26
|
+
});
|
|
27
|
+
return (await response.json());
|
|
28
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { ResourceCtx } from '../../../core/extend-resource.js';
|
|
2
|
+
/**
|
|
3
|
+
* Progress reported on each poll where the fleet has NOT yet fully
|
|
4
|
+
* converged on the target release. `onLatest` of `total` relevant
|
|
5
|
+
* dynos are running a version `>= version`.
|
|
6
|
+
*/
|
|
7
|
+
export type WaitForReleaseProgress = {
|
|
8
|
+
onLatest: number;
|
|
9
|
+
total: number;
|
|
10
|
+
version: number;
|
|
11
|
+
};
|
|
12
|
+
export type WaitForReleaseOptions = {
|
|
13
|
+
/**
|
|
14
|
+
* Maximum number of polls before giving up with
|
|
15
|
+
* {@link ReleaseNotConvergedError}. Omitted (the default) means
|
|
16
|
+
* unbounded — the wait runs until the fleet converges or the
|
|
17
|
+
* caller's {@link WaitForReleaseOptions.signal} aborts, matching a
|
|
18
|
+
* long-running `ps:wait`.
|
|
19
|
+
*/
|
|
20
|
+
attempts?: number;
|
|
21
|
+
/** Delay between polls in milliseconds. Defaults to 10000. */
|
|
22
|
+
delayMs?: number;
|
|
23
|
+
/**
|
|
24
|
+
* Fires after every poll where the fleet has NOT yet converged
|
|
25
|
+
* (i.e. `onLatest < total`), before the next wait. It never fires
|
|
26
|
+
* on the converged poll — a fleet already on the latest release
|
|
27
|
+
* when the first poll runs produces no `onPoll` calls at all. Use
|
|
28
|
+
* it to drive a live progress indicator; the converged result is
|
|
29
|
+
* the resolved value, not an `onPoll` call.
|
|
30
|
+
*/
|
|
31
|
+
onPoll?: (progress: WaitForReleaseProgress) => void;
|
|
32
|
+
signal?: AbortSignal;
|
|
33
|
+
/** Only wait for dynos of this process type. */
|
|
34
|
+
type?: string;
|
|
35
|
+
/** Include one-off `run` dynos in the wait. Mutually exclusive with `type`. */
|
|
36
|
+
withRun?: boolean;
|
|
37
|
+
};
|
|
38
|
+
export type WaitForReleaseResult = {
|
|
39
|
+
total: number;
|
|
40
|
+
version: number;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Thrown by {@link waitForRelease} when {@link WaitForReleaseOptions.attempts}
|
|
44
|
+
* was set and the fleet did not converge on the target release within
|
|
45
|
+
* that many polls. Carries the last observed progress so callers can
|
|
46
|
+
* report how far the fleet got.
|
|
47
|
+
*/
|
|
48
|
+
export declare class ReleaseNotConvergedError extends Error {
|
|
49
|
+
readonly attempts: number;
|
|
50
|
+
readonly progress: undefined | WaitForReleaseProgress;
|
|
51
|
+
readonly id = "release_not_converged";
|
|
52
|
+
constructor(attempts: number, progress: undefined | WaitForReleaseProgress);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Poll an app's dynos until every relevant one is `up` and running a
|
|
56
|
+
* release version at least the app's latest, then resolve with the
|
|
57
|
+
* converged `{total, version}`.
|
|
58
|
+
*
|
|
59
|
+
* "Relevant" excludes `release` dynos always, and one-off `run` dynos
|
|
60
|
+
* unless {@link WaitForReleaseOptions.withRun} is set or
|
|
61
|
+
* {@link WaitForReleaseOptions.type} names `'run'`; when
|
|
62
|
+
* {@link WaitForReleaseOptions.type} is given, only that process type
|
|
63
|
+
* is considered.
|
|
64
|
+
*
|
|
65
|
+
* The latest release is resolved once up front via
|
|
66
|
+
* `GET /apps/{app}/releases` scoped to `Range: version ..; max=1,
|
|
67
|
+
* order=desc`. If the app has no releases (or the latest has no
|
|
68
|
+
* version), resolves `undefined` without polling — the caller should
|
|
69
|
+
* report "no releases" rather than wait forever.
|
|
70
|
+
*
|
|
71
|
+
* Polling is unbounded by default (runs until convergence or an
|
|
72
|
+
* aborted {@link WaitForReleaseOptions.signal}); pass
|
|
73
|
+
* {@link WaitForReleaseOptions.attempts} to cap it, after which it
|
|
74
|
+
* throws {@link ReleaseNotConvergedError}.
|
|
75
|
+
*/
|
|
76
|
+
export declare function waitForRelease(ctx: Pick<ResourceCtx, 'platform'>, appIdentity: string, options?: WaitForReleaseOptions): Promise<undefined | WaitForReleaseResult>;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { wait } from '../../../utils/wait.js';
|
|
2
|
+
const DEFAULT_DELAY_MS = 10_000;
|
|
3
|
+
const LATEST_RELEASE_RANGE = 'version ..; max=1, order=desc';
|
|
4
|
+
/**
|
|
5
|
+
* Thrown by {@link waitForRelease} when {@link WaitForReleaseOptions.attempts}
|
|
6
|
+
* was set and the fleet did not converge on the target release within
|
|
7
|
+
* that many polls. Carries the last observed progress so callers can
|
|
8
|
+
* report how far the fleet got.
|
|
9
|
+
*/
|
|
10
|
+
export class ReleaseNotConvergedError extends Error {
|
|
11
|
+
attempts;
|
|
12
|
+
progress;
|
|
13
|
+
id = 'release_not_converged';
|
|
14
|
+
constructor(attempts, progress) {
|
|
15
|
+
super(`Fleet did not converge on release version ${progress?.version ?? '?'} `
|
|
16
|
+
+ `within ${attempts} attempts`
|
|
17
|
+
+ (progress ? ` (last: ${progress.onLatest}/${progress.total} on latest).` : '.'));
|
|
18
|
+
this.attempts = attempts;
|
|
19
|
+
this.progress = progress;
|
|
20
|
+
this.name = 'ReleaseNotConvergedError';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Poll an app's dynos until every relevant one is `up` and running a
|
|
25
|
+
* release version at least the app's latest, then resolve with the
|
|
26
|
+
* converged `{total, version}`.
|
|
27
|
+
*
|
|
28
|
+
* "Relevant" excludes `release` dynos always, and one-off `run` dynos
|
|
29
|
+
* unless {@link WaitForReleaseOptions.withRun} is set or
|
|
30
|
+
* {@link WaitForReleaseOptions.type} names `'run'`; when
|
|
31
|
+
* {@link WaitForReleaseOptions.type} is given, only that process type
|
|
32
|
+
* is considered.
|
|
33
|
+
*
|
|
34
|
+
* The latest release is resolved once up front via
|
|
35
|
+
* `GET /apps/{app}/releases` scoped to `Range: version ..; max=1,
|
|
36
|
+
* order=desc`. If the app has no releases (or the latest has no
|
|
37
|
+
* version), resolves `undefined` without polling — the caller should
|
|
38
|
+
* report "no releases" rather than wait forever.
|
|
39
|
+
*
|
|
40
|
+
* Polling is unbounded by default (runs until convergence or an
|
|
41
|
+
* aborted {@link WaitForReleaseOptions.signal}); pass
|
|
42
|
+
* {@link WaitForReleaseOptions.attempts} to cap it, after which it
|
|
43
|
+
* throws {@link ReleaseNotConvergedError}.
|
|
44
|
+
*/
|
|
45
|
+
export async function waitForRelease(ctx, appIdentity, options = {}) {
|
|
46
|
+
const { attempts, delayMs = DEFAULT_DELAY_MS, onPoll, signal, type, withRun, } = options;
|
|
47
|
+
signal?.throwIfAborted();
|
|
48
|
+
const platform = signal ? ctx.platform.withOptions({ signal }) : ctx.platform;
|
|
49
|
+
const releases = await platform
|
|
50
|
+
.withHeaders({ Range: LATEST_RELEASE_RANGE })
|
|
51
|
+
.release.list(appIdentity);
|
|
52
|
+
const version = releases[0]?.version;
|
|
53
|
+
if (version === undefined) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
let lastProgress;
|
|
57
|
+
let attempt = 0;
|
|
58
|
+
while (true) {
|
|
59
|
+
const dynos = await platform.dyno.list(appIdentity);
|
|
60
|
+
const relevant = dynos
|
|
61
|
+
.filter(dyno => dyno.type !== 'release')
|
|
62
|
+
// Keep run dynos only when explicitly requested — via withRun, or a
|
|
63
|
+
// type filter that names 'run'. Ordering matters: this must not drop
|
|
64
|
+
// run dynos before the type filter below can select them.
|
|
65
|
+
.filter(dyno => withRun || type === 'run' || dyno.type !== 'run')
|
|
66
|
+
.filter(dyno => !type || dyno.type === type);
|
|
67
|
+
const onLatest = relevant.filter(dyno => (dyno.state === 'up'
|
|
68
|
+
&& dyno.release?.version !== undefined
|
|
69
|
+
&& dyno.release.version >= version)).length;
|
|
70
|
+
if (onLatest === relevant.length) {
|
|
71
|
+
return { total: relevant.length, version };
|
|
72
|
+
}
|
|
73
|
+
lastProgress = { onLatest, total: relevant.length, version };
|
|
74
|
+
onPoll?.(lastProgress);
|
|
75
|
+
// Give up once the (opt-in) attempts cap is reached, before the next
|
|
76
|
+
// wait — an unbounded wait only exits on convergence or an aborted signal.
|
|
77
|
+
attempt++;
|
|
78
|
+
if (attempts !== undefined && attempt >= attempts) {
|
|
79
|
+
throw new ReleaseNotConvergedError(attempts, lastProgress);
|
|
80
|
+
}
|
|
81
|
+
await wait(delayMs, signal);
|
|
82
|
+
}
|
|
83
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroku/sdk",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
4
4
|
"description": "Heroku SDK",
|
|
5
5
|
"author": "Heroku",
|
|
6
6
|
"type": "module",
|
|
@@ -145,7 +145,7 @@
|
|
|
145
145
|
},
|
|
146
146
|
"dependencies": {
|
|
147
147
|
"@heroku/heroku-fetch": "^0.1.4",
|
|
148
|
-
"@heroku/types": "^
|
|
148
|
+
"@heroku/types": "^5.0.0",
|
|
149
149
|
"debug": "^4.4.0"
|
|
150
150
|
}
|
|
151
151
|
}
|