@heroku/sdk 0.1.0-beta.0 → 0.3.0-beta.0
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 +1 -1
- package/dist/resources/data/maintenance.d.ts +1 -1
- package/dist/resources/data/postgres-database.d.ts +1 -1
- package/dist/resources/data/transfer.d.ts +1 -1
- package/dist/resources/platform/add-on/index.d.ts +1 -1
- package/dist/resources/platform/app/index.d.ts +1 -1
- package/dist/resources/platform/dyno/index.d.ts +1 -1
- package/dist/resources/platform/log-session/index.d.ts +1 -1
- package/dist/resources/platform/pipeline/coupling.d.ts +1 -1
- package/dist/resources/platform/pipeline/index.d.ts +1 -1
- package/dist/resources/platform/pipeline/promotion.d.ts +1 -1
- package/package.json +25 -7
- package/dist/compositions/add-on.d.ts +0 -7
- package/dist/compositions/add-on.js +0 -6
- package/dist/compositions/app.d.ts +0 -8
- package/dist/compositions/app.js +0 -11
- package/dist/compositions/dyno.d.ts +0 -17
- package/dist/compositions/dyno.js +0 -23
- package/dist/compositions/pg.d.ts +0 -15
- package/dist/compositions/pg.js +0 -56
- package/dist/compositions/pipeline.d.ts +0 -33
- package/dist/compositions/pipeline.js +0 -146
- package/dist/resources/data/backup.d.ts +0 -9
- package/dist/resources/data/backup.js +0 -10
- package/dist/resources/data/internal/resolve-addon-id.d.ts +0 -2
- package/dist/resources/data/internal/resolve-addon-id.js +0 -13
- package/dist/resources/data/postgres-credential.d.ts +0 -11
- package/dist/resources/data/postgres-credential.js +0 -16
- package/dist/resources/data/postgres.d.ts +0 -9
- package/dist/resources/data/postgres.js +0 -10
- package/dist/resources/data/transfer-schedule.d.ts +0 -9
- package/dist/resources/data/transfer-schedule.js +0 -10
- package/dist/resources/platform/add-on.d.ts +0 -211
- package/dist/resources/platform/add-on.js +0 -354
- package/dist/resources/platform/app.d.ts +0 -56
- package/dist/resources/platform/app.js +0 -50
- package/dist/resources/platform/dyno.d.ts +0 -45
- package/dist/resources/platform/dyno.js +0 -26
- package/dist/resources/platform/log-session.d.ts +0 -81
- package/dist/resources/platform/log-session.js +0 -167
- package/dist/resources/platform/pipeline-coupling.d.ts +0 -27
- package/dist/resources/platform/pipeline-coupling.js +0 -55
- package/dist/resources/platform/pipeline-promotion.d.ts +0 -21
- package/dist/resources/platform/pipeline-promotion.js +0 -128
- package/dist/resources/platform/pipeline.d.ts +0 -53
- package/dist/resources/platform/pipeline.js +0 -75
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import type { Formation } from '@heroku/types/3.sdk';
|
|
2
|
-
import type { ResourceCtx } from '../../core/extend-resource.js';
|
|
3
|
-
import type { StickyRouteOptions } from '../../core/create-client.js';
|
|
4
|
-
export type DynoOptions = {
|
|
5
|
-
signal?: AbortSignal;
|
|
6
|
-
};
|
|
7
|
-
export type ScaleDynosUpdate = {
|
|
8
|
-
dyno_size?: {
|
|
9
|
-
id?: string;
|
|
10
|
-
name?: string;
|
|
11
|
-
};
|
|
12
|
-
quantity?: number | string;
|
|
13
|
-
size?: string;
|
|
14
|
-
type: string;
|
|
15
|
-
};
|
|
16
|
-
export type RestartDynosTarget = {
|
|
17
|
-
dyno: string;
|
|
18
|
-
} | {
|
|
19
|
-
type: string;
|
|
20
|
-
};
|
|
21
|
-
type FormationClient = {
|
|
22
|
-
batchUpdate(appIdentity: string, body: {
|
|
23
|
-
updates: ScaleDynosUpdate[];
|
|
24
|
-
}): Promise<Formation[]>;
|
|
25
|
-
update(appIdentity: string, type: string, body: Omit<ScaleDynosUpdate, 'type'>): Promise<Formation>;
|
|
26
|
-
};
|
|
27
|
-
type ScalePlatform = {
|
|
28
|
-
formation: FormationClient;
|
|
29
|
-
withOptions(opts: StickyRouteOptions): ScalePlatform;
|
|
30
|
-
};
|
|
31
|
-
export declare function scaleDynos(ctx: {
|
|
32
|
-
platform: ScalePlatform;
|
|
33
|
-
}, appIdentity: string, updates: ScaleDynosUpdate, options?: DynoOptions): Promise<Formation>;
|
|
34
|
-
export declare function scaleDynos(ctx: {
|
|
35
|
-
platform: ScalePlatform;
|
|
36
|
-
}, appIdentity: string, updates: ScaleDynosUpdate[], options?: DynoOptions): Promise<Formation[]>;
|
|
37
|
-
export declare function restartDynos(ctx: Pick<ResourceCtx, 'platform'>, appIdentity: string, target?: RestartDynosTarget, options?: DynoOptions): Promise<void>;
|
|
38
|
-
export declare const dynoExtensions: import("../../index.js").ResourceExtension<"platform", "dyno", {
|
|
39
|
-
restart: (appIdentity: string, target?: RestartDynosTarget, options?: DynoOptions) => Promise<void>;
|
|
40
|
-
scale: {
|
|
41
|
-
(appIdentity: string, updates: ScaleDynosUpdate, options?: DynoOptions): Promise<Formation>;
|
|
42
|
-
(appIdentity: string, updates: ScaleDynosUpdate[], options?: DynoOptions): Promise<Formation[]>;
|
|
43
|
-
};
|
|
44
|
-
}>;
|
|
45
|
-
export {};
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { extendResource } from '../../core/extend-resource.js';
|
|
2
|
-
export async function scaleDynos(ctx, appIdentity, updates, options = {}) {
|
|
3
|
-
options.signal?.throwIfAborted();
|
|
4
|
-
const platform = options.signal ? ctx.platform.withOptions({ signal: options.signal }) : ctx.platform;
|
|
5
|
-
if (Array.isArray(updates)) {
|
|
6
|
-
return platform.formation.batchUpdate(appIdentity, { updates });
|
|
7
|
-
}
|
|
8
|
-
const { type, ...body } = updates;
|
|
9
|
-
return platform.formation.update(appIdentity, type, body);
|
|
10
|
-
}
|
|
11
|
-
export async function restartDynos(ctx, appIdentity, target, options = {}) {
|
|
12
|
-
options.signal?.throwIfAborted();
|
|
13
|
-
if (!target) {
|
|
14
|
-
await ctx.platform.dyno.restartAll(appIdentity);
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
17
|
-
if ('dyno' in target) {
|
|
18
|
-
await ctx.platform.dyno.restart(appIdentity, target.dyno);
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
await ctx.platform.dyno.restartFormation(appIdentity, target.type);
|
|
22
|
-
}
|
|
23
|
-
export const dynoExtensions = extendResource('platform', 'dyno', ctx => ({
|
|
24
|
-
restart: (appIdentity, target, options) => (restartDynos(ctx, appIdentity, target, options)),
|
|
25
|
-
scale: ((appIdentity, updates, options) => scaleDynos(ctx, appIdentity, updates, options)),
|
|
26
|
-
}));
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import type { ResourceCtx } from '../../core/extend-resource.js';
|
|
2
|
-
export type StreamLogsOptions = {
|
|
3
|
-
/** Limit output to a single dyno (e.g. `web.1` on Cedar, `web-abc-123` on Fir). */
|
|
4
|
-
dyno?: string;
|
|
5
|
-
/**
|
|
6
|
-
* Number of recent lines to fetch before tailing. Cedar-generation
|
|
7
|
-
* apps only — silently ignored on Fir.
|
|
8
|
-
*/
|
|
9
|
-
lines?: number;
|
|
10
|
-
/**
|
|
11
|
-
* Fires once per session creation, before the SDK opens the
|
|
12
|
-
* `logplex_url` stream. Receives the resolved generation so the
|
|
13
|
-
* caller can surface generation-specific UX (e.g. "Fetching
|
|
14
|
-
* logs..." for Fir, where provisioning the stream takes a moment).
|
|
15
|
-
*
|
|
16
|
-
* Fires on every recreate when tailing.
|
|
17
|
-
*/
|
|
18
|
-
onSessionCreated?: (info: {
|
|
19
|
-
generation: 'cedar' | 'fir' | undefined;
|
|
20
|
-
isRecreate: boolean;
|
|
21
|
-
}) => Promise<void> | void;
|
|
22
|
-
/**
|
|
23
|
-
* When `tail` is true, watch for the platform's idle timeout and
|
|
24
|
-
* recreate the log session to keep streaming. Defaults to true.
|
|
25
|
-
* Set to false if you want a single bounded stream and surface the
|
|
26
|
-
* disconnect to the caller.
|
|
27
|
-
*/
|
|
28
|
-
recreateSession?: boolean;
|
|
29
|
-
/**
|
|
30
|
-
* If `recreateSession` is true, the SDK forces a recreate after
|
|
31
|
-
* this many milliseconds without seeing data. Defaults to
|
|
32
|
-
* 15 minutes (matches the Fir platform timeout).
|
|
33
|
-
*/
|
|
34
|
-
sessionTimeoutMs?: number;
|
|
35
|
-
signal?: AbortSignal;
|
|
36
|
-
/** Limit output to a single source (e.g. `app`, `heroku`). */
|
|
37
|
-
source?: string;
|
|
38
|
-
/**
|
|
39
|
-
* If true, the platform keeps the stream open and pushes lines as
|
|
40
|
-
* they arrive. The platform may close the connection on its own
|
|
41
|
-
* cadence (Fir is documented to time out around 15 minutes); when
|
|
42
|
-
* `recreateSession` is also true, the SDK transparently opens a
|
|
43
|
-
* new session and continues yielding lines.
|
|
44
|
-
*
|
|
45
|
-
* Defaults to false. Note: Fir-generation apps always tail (the
|
|
46
|
-
* platform doesn't support a bounded log session there), so the
|
|
47
|
-
* SDK forces `tail: true` for Fir regardless of what the caller
|
|
48
|
-
* asked for.
|
|
49
|
-
*/
|
|
50
|
-
tail?: boolean;
|
|
51
|
-
/**
|
|
52
|
-
* Limit output to a process type (e.g. `web`, `worker`). On Cedar
|
|
53
|
-
* the underlying API combines `dyno` and `type` into a single
|
|
54
|
-
* field; the SDK handles that translation for you.
|
|
55
|
-
*/
|
|
56
|
-
type?: string;
|
|
57
|
-
};
|
|
58
|
-
/**
|
|
59
|
-
* Stream logs for an app, yielding one line at a time.
|
|
60
|
-
*
|
|
61
|
-
* Creates a log session via `logSession.create`, opens the resulting
|
|
62
|
-
* `logplex_url`, and parses the chunked plain-text response into
|
|
63
|
-
* newline-delimited entries. Use `tail: true` to keep the stream
|
|
64
|
-
* open; `recreateSession: true` (the default when tailing) makes the
|
|
65
|
-
* SDK transparently re-open a new session after the platform's idle
|
|
66
|
-
* timeout, so the iterator keeps yielding without the caller having
|
|
67
|
-
* to handle the reconnect.
|
|
68
|
-
*
|
|
69
|
-
* The iterator terminates cleanly when:
|
|
70
|
-
* - the platform closes a non-tailing stream (all lines yielded)
|
|
71
|
-
* - `signal` is aborted (an `AbortError` propagates)
|
|
72
|
-
* - `recreateSession` is false and the platform closes a tail
|
|
73
|
-
*
|
|
74
|
-
* The iterator throws on:
|
|
75
|
-
* - non-2xx response from the logplex URL
|
|
76
|
-
* - errors thrown by `logSession.create` (auth, app-not-found, etc.)
|
|
77
|
-
*/
|
|
78
|
-
export declare function streamLogs(ctx: Pick<ResourceCtx, 'platform'>, appIdentity: string, options?: StreamLogsOptions): AsyncGenerator<string, void, unknown>;
|
|
79
|
-
export declare const logSessionExtensions: import("../../index.js").ResourceExtension<"platform", "logSession", {
|
|
80
|
-
streamLogs: (appIdentity: string, options?: StreamLogsOptions) => AsyncGenerator<string, void, unknown>;
|
|
81
|
-
}>;
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
import { HerokuApiClient } from '@heroku/heroku-fetch';
|
|
2
|
-
import createDebug from 'debug';
|
|
3
|
-
import { extendResource } from '../../core/extend-resource.js';
|
|
4
|
-
import { getGeneration } from './app.js';
|
|
5
|
-
const debug = createDebug('heroku:sdk:resources:log-session');
|
|
6
|
-
const DEFAULT_FIR_SESSION_TIMEOUT_MS = 15 * 60 * 1000;
|
|
7
|
-
const FIR_TIMEOUT_ERROR_MESSAGE = 'Fir log stream timeout';
|
|
8
|
-
/**
|
|
9
|
-
* Stream logs for an app, yielding one line at a time.
|
|
10
|
-
*
|
|
11
|
-
* Creates a log session via `logSession.create`, opens the resulting
|
|
12
|
-
* `logplex_url`, and parses the chunked plain-text response into
|
|
13
|
-
* newline-delimited entries. Use `tail: true` to keep the stream
|
|
14
|
-
* open; `recreateSession: true` (the default when tailing) makes the
|
|
15
|
-
* SDK transparently re-open a new session after the platform's idle
|
|
16
|
-
* timeout, so the iterator keeps yielding without the caller having
|
|
17
|
-
* to handle the reconnect.
|
|
18
|
-
*
|
|
19
|
-
* The iterator terminates cleanly when:
|
|
20
|
-
* - the platform closes a non-tailing stream (all lines yielded)
|
|
21
|
-
* - `signal` is aborted (an `AbortError` propagates)
|
|
22
|
-
* - `recreateSession` is false and the platform closes a tail
|
|
23
|
-
*
|
|
24
|
-
* The iterator throws on:
|
|
25
|
-
* - non-2xx response from the logplex URL
|
|
26
|
-
* - errors thrown by `logSession.create` (auth, app-not-found, etc.)
|
|
27
|
-
*/
|
|
28
|
-
export async function* streamLogs(ctx, appIdentity, options = {}) {
|
|
29
|
-
const { dyno, lines, sessionTimeoutMs = DEFAULT_FIR_SESSION_TIMEOUT_MS, signal, source, tail = false, type, } = options;
|
|
30
|
-
signal?.throwIfAborted();
|
|
31
|
-
const generation = await getGeneration(ctx, appIdentity, { signal });
|
|
32
|
-
const isFir = generation === 'fir';
|
|
33
|
-
// Fir doesn't support a bounded session; the platform always streams.
|
|
34
|
-
const effectiveTail = isFir ? true : tail;
|
|
35
|
-
const recreateSession = options.recreateSession ?? effectiveTail;
|
|
36
|
-
const createOpts = buildCreateOpts({
|
|
37
|
-
dyno, isFir, lines, source, tail: effectiveTail, type,
|
|
38
|
-
});
|
|
39
|
-
// The recreate loop is inherently sequential: each session must
|
|
40
|
-
// close before we ask for the next one.
|
|
41
|
-
/* eslint-disable no-await-in-loop */
|
|
42
|
-
let isRecreate = false;
|
|
43
|
-
while (true) {
|
|
44
|
-
debug('streamLogs creating session app=%s generation=%s tail=%s', appIdentity, generation ?? '<unknown>', effectiveTail);
|
|
45
|
-
const session = await ctx.platform.logSession.create(appIdentity, createOpts);
|
|
46
|
-
if (!session.logplex_url) {
|
|
47
|
-
throw new Error('Log session response did not include a logplex_url.');
|
|
48
|
-
}
|
|
49
|
-
await options.onSessionCreated?.({ generation, isRecreate });
|
|
50
|
-
isRecreate = true;
|
|
51
|
-
debug('streamLogs session=%s opening stream', session.id);
|
|
52
|
-
let timedOut = false;
|
|
53
|
-
let timeoutHandle;
|
|
54
|
-
try {
|
|
55
|
-
// Logplex URLs hit a region-specific stream host (e.g.
|
|
56
|
-
// virginia.sessions.logs.heroku.com). The token is embedded in
|
|
57
|
-
// the URL, so we use a custom-service client with no auth and
|
|
58
|
-
// a per-call baseUrl; this also picks up heroku-fetch's
|
|
59
|
-
// env-var proxy support.
|
|
60
|
-
const parsed = new URL(session.logplex_url);
|
|
61
|
-
const logplexClient = new HerokuApiClient({
|
|
62
|
-
baseUrl: parsed.origin,
|
|
63
|
-
service: 'custom',
|
|
64
|
-
token: '',
|
|
65
|
-
});
|
|
66
|
-
const response = await logplexClient.stream(`${parsed.pathname}${parsed.search}`);
|
|
67
|
-
if (!response.body) {
|
|
68
|
-
throw new Error('Logplex stream returned no body.');
|
|
69
|
-
}
|
|
70
|
-
const reader = response.body.getReader();
|
|
71
|
-
const decoder = new TextDecoder();
|
|
72
|
-
let pending = '';
|
|
73
|
-
// Force a recreate after sessionTimeoutMs of silence; ignored
|
|
74
|
-
// when not tailing.
|
|
75
|
-
const armTimeout = () => {
|
|
76
|
-
if (!recreateSession)
|
|
77
|
-
return;
|
|
78
|
-
if (timeoutHandle)
|
|
79
|
-
clearTimeout(timeoutHandle);
|
|
80
|
-
timeoutHandle = setTimeout(() => {
|
|
81
|
-
timedOut = true;
|
|
82
|
-
debug('streamLogs session=%s timed out after %dms', session.id, sessionTimeoutMs);
|
|
83
|
-
reader.cancel().catch(() => { });
|
|
84
|
-
}, sessionTimeoutMs);
|
|
85
|
-
};
|
|
86
|
-
armTimeout();
|
|
87
|
-
try {
|
|
88
|
-
while (true) {
|
|
89
|
-
const { done, value } = await reader.read();
|
|
90
|
-
if (done)
|
|
91
|
-
break;
|
|
92
|
-
armTimeout();
|
|
93
|
-
pending += decoder.decode(value, { stream: true });
|
|
94
|
-
let newlineIndex = pending.indexOf('\n');
|
|
95
|
-
while (newlineIndex !== -1) {
|
|
96
|
-
const line = pending.slice(0, newlineIndex);
|
|
97
|
-
pending = pending.slice(newlineIndex + 1);
|
|
98
|
-
if (line.length > 0)
|
|
99
|
-
yield line;
|
|
100
|
-
newlineIndex = pending.indexOf('\n');
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
// Flush any trailing partial line that didn't end in '\n'.
|
|
104
|
-
const trailing = pending + decoder.decode();
|
|
105
|
-
if (trailing.length > 0)
|
|
106
|
-
yield trailing;
|
|
107
|
-
}
|
|
108
|
-
finally {
|
|
109
|
-
if (timeoutHandle)
|
|
110
|
-
clearTimeout(timeoutHandle);
|
|
111
|
-
reader.releaseLock();
|
|
112
|
-
}
|
|
113
|
-
if (timedOut) {
|
|
114
|
-
debug('streamLogs session=%s recreating after timeout', session.id);
|
|
115
|
-
continue;
|
|
116
|
-
}
|
|
117
|
-
if (!recreateSession) {
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
debug('streamLogs session=%s remote closed; recreating', session.id);
|
|
121
|
-
}
|
|
122
|
-
catch (error) {
|
|
123
|
-
if (timeoutHandle)
|
|
124
|
-
clearTimeout(timeoutHandle);
|
|
125
|
-
if (signal?.aborted)
|
|
126
|
-
throw error;
|
|
127
|
-
if (timedOut)
|
|
128
|
-
continue;
|
|
129
|
-
// Translate Fir's documented timeout error message into a recreate.
|
|
130
|
-
if (error instanceof Error && error.message === FIR_TIMEOUT_ERROR_MESSAGE && recreateSession) {
|
|
131
|
-
continue;
|
|
132
|
-
}
|
|
133
|
-
throw error;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
/* eslint-enable no-await-in-loop */
|
|
137
|
-
}
|
|
138
|
-
/**
|
|
139
|
-
* Translate the SDK's consumer-facing options into the shape the
|
|
140
|
-
* platform's `log-sessions` endpoint expects per generation:
|
|
141
|
-
* - Cedar's session takes a single `dyno` field that historically
|
|
142
|
-
* accepted either a process type or a specific dyno; we collapse
|
|
143
|
-
* `dyno || type` for the caller. `lines` and `tail` are honored.
|
|
144
|
-
* - Fir's session takes `dyno` and `type` separately and ignores
|
|
145
|
-
* `lines`; the stream always tails.
|
|
146
|
-
*/
|
|
147
|
-
function buildCreateOpts(options) {
|
|
148
|
-
const createOpts = { source: options.source };
|
|
149
|
-
if (options.isFir) {
|
|
150
|
-
if (options.dyno)
|
|
151
|
-
createOpts.dyno = options.dyno;
|
|
152
|
-
if (options.type)
|
|
153
|
-
createOpts.type = options.type;
|
|
154
|
-
}
|
|
155
|
-
else {
|
|
156
|
-
const cedarDyno = options.dyno ?? options.type;
|
|
157
|
-
if (cedarDyno)
|
|
158
|
-
createOpts.dyno = cedarDyno;
|
|
159
|
-
if (options.lines !== undefined)
|
|
160
|
-
createOpts.lines = options.lines;
|
|
161
|
-
createOpts.tail = options.tail;
|
|
162
|
-
}
|
|
163
|
-
return createOpts;
|
|
164
|
-
}
|
|
165
|
-
export const logSessionExtensions = extendResource('platform', 'logSession', ctx => ({
|
|
166
|
-
streamLogs: (appIdentity, options) => streamLogs(ctx, appIdentity, options),
|
|
167
|
-
}));
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import type { PipelineCoupling, TeamApp } from '@heroku/types/3.sdk';
|
|
2
|
-
import type { ResourceCtx } from '../../core/extend-resource.js';
|
|
3
|
-
export type PipelineWarning = {
|
|
4
|
-
limit: number;
|
|
5
|
-
pipelineId: string;
|
|
6
|
-
type: 'apps_truncated';
|
|
7
|
-
};
|
|
8
|
-
export type ListPipelineAppsOptions = {
|
|
9
|
-
onWarning?: (warning: PipelineWarning) => void;
|
|
10
|
-
signal?: AbortSignal;
|
|
11
|
-
};
|
|
12
|
-
export type AppWithPipelineCoupling = TeamApp & {
|
|
13
|
-
pipelineCoupling: PipelineCoupling;
|
|
14
|
-
};
|
|
15
|
-
/**
|
|
16
|
-
* List the apps coupled to a pipeline, each decorated with its
|
|
17
|
-
* `PipelineCoupling`.
|
|
18
|
-
*
|
|
19
|
-
* Couplings missing an app id are dropped before issuing the bulk
|
|
20
|
-
* filter call. If the pipeline has more than 1000 couplings, an
|
|
21
|
-
* `onWarning` handler must be supplied to opt into a truncated
|
|
22
|
-
* result; otherwise this throws.
|
|
23
|
-
*/
|
|
24
|
-
export declare function listPipelineApps(ctx: Pick<ResourceCtx, 'platform'>, pipelineId: string, options?: ListPipelineAppsOptions): Promise<AppWithPipelineCoupling[]>;
|
|
25
|
-
export declare const pipelineCouplingExtensions: import("../../index.js").ResourceExtension<"platform", "pipelineCoupling", {
|
|
26
|
-
listApps: (pipelineId: string, options?: ListPipelineAppsOptions) => Promise<AppWithPipelineCoupling[]>;
|
|
27
|
-
}>;
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import createDebug from 'debug';
|
|
2
|
-
import { extendResource } from '../../core/extend-resource.js';
|
|
3
|
-
const debug = createDebug('heroku:sdk:resources:pipeline-coupling');
|
|
4
|
-
const APPS_FILTER_LIMIT = 1000;
|
|
5
|
-
/**
|
|
6
|
-
* List the apps coupled to a pipeline, each decorated with its
|
|
7
|
-
* `PipelineCoupling`.
|
|
8
|
-
*
|
|
9
|
-
* Couplings missing an app id are dropped before issuing the bulk
|
|
10
|
-
* filter call. If the pipeline has more than 1000 couplings, an
|
|
11
|
-
* `onWarning` handler must be supplied to opt into a truncated
|
|
12
|
-
* result; otherwise this throws.
|
|
13
|
-
*/
|
|
14
|
-
export async function listPipelineApps(ctx, pipelineId, options = {}) {
|
|
15
|
-
options.signal?.throwIfAborted();
|
|
16
|
-
const allCouplings = await ctx.platform.pipelineCoupling.listByPipeline(pipelineId);
|
|
17
|
-
// Drop malformed couplings (no app id) before issuing the bulk filter call.
|
|
18
|
-
const couplings = allCouplings.filter(coupling => coupling.app?.id);
|
|
19
|
-
const skipped = allCouplings.length - couplings.length;
|
|
20
|
-
if (skipped > 0) {
|
|
21
|
-
debug('listPipelineApps pipeline=%s skipped=%d couplings without app.id', pipelineId, skipped);
|
|
22
|
-
}
|
|
23
|
-
if (couplings.length === 0) {
|
|
24
|
-
return [];
|
|
25
|
-
}
|
|
26
|
-
let couplingsToResolve = couplings;
|
|
27
|
-
if (couplings.length > APPS_FILTER_LIMIT) {
|
|
28
|
-
if (!options.onWarning) {
|
|
29
|
-
throw new Error(`Pipeline ${pipelineId} has more than ${APPS_FILTER_LIMIT} apps. `
|
|
30
|
-
+ 'Pass an onWarning handler to opt into a truncated result.');
|
|
31
|
-
}
|
|
32
|
-
debug('listPipelineApps pipeline=%s truncating couplings=%d limit=%d', pipelineId, couplings.length, APPS_FILTER_LIMIT);
|
|
33
|
-
options.onWarning({ limit: APPS_FILTER_LIMIT, pipelineId, type: 'apps_truncated' });
|
|
34
|
-
couplingsToResolve = couplings.slice(0, APPS_FILTER_LIMIT);
|
|
35
|
-
}
|
|
36
|
-
options.signal?.throwIfAborted();
|
|
37
|
-
const ids = couplingsToResolve.map(coupling => coupling.app.id);
|
|
38
|
-
const apps = await ctx.platform
|
|
39
|
-
.withHeaders({ Range: `id ..; max=${APPS_FILTER_LIMIT};` })
|
|
40
|
-
.filterApps.apps({ in: { id: ids } });
|
|
41
|
-
if (apps.length !== ids.length) {
|
|
42
|
-
debug('listPipelineApps pipeline=%s requested=%d returned=%d', pipelineId, ids.length, apps.length);
|
|
43
|
-
}
|
|
44
|
-
const couplingByAppId = new Map();
|
|
45
|
-
for (const coupling of couplingsToResolve) {
|
|
46
|
-
couplingByAppId.set(coupling.app.id, coupling);
|
|
47
|
-
}
|
|
48
|
-
return apps.map(app => ({
|
|
49
|
-
...app,
|
|
50
|
-
pipelineCoupling: couplingByAppId.get(app.id),
|
|
51
|
-
}));
|
|
52
|
-
}
|
|
53
|
-
export const pipelineCouplingExtensions = extendResource('platform', 'pipelineCoupling', ctx => ({
|
|
54
|
-
listApps: (pipelineId, options) => listPipelineApps(ctx, pipelineId, options),
|
|
55
|
-
}));
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import type { PipelinePromotion, PipelinePromotionCreateOpts, PipelinePromotionTarget } from '@heroku/types/3.sdk';
|
|
2
|
-
import type { ResourceCtx } from '../../core/extend-resource.js';
|
|
3
|
-
export type ReleaseStreamContext = {
|
|
4
|
-
stream: ReadableStream<Uint8Array>;
|
|
5
|
-
target: PipelinePromotionTarget;
|
|
6
|
-
};
|
|
7
|
-
export type PromotePipelineOptions = {
|
|
8
|
-
intervalMs?: number;
|
|
9
|
-
onReleaseStream?: (context: ReleaseStreamContext) => Promise<void> | void;
|
|
10
|
-
releaseStreamMaxAttempts?: number;
|
|
11
|
-
signal?: AbortSignal;
|
|
12
|
-
timeoutMs?: number;
|
|
13
|
-
};
|
|
14
|
-
export type PromotePipelineResult = {
|
|
15
|
-
promotion: PipelinePromotion;
|
|
16
|
-
targets: PipelinePromotionTarget[];
|
|
17
|
-
};
|
|
18
|
-
export declare function promotePipeline(ctx: Pick<ResourceCtx, 'platform'>, body: PipelinePromotionCreateOpts, options?: PromotePipelineOptions): Promise<PromotePipelineResult>;
|
|
19
|
-
export declare const pipelinePromotionExtensions: import("../../index.js").ResourceExtension<"platform", "pipelinePromotion", {
|
|
20
|
-
promote: (body: PipelinePromotionCreateOpts, options?: PromotePipelineOptions) => Promise<PromotePipelineResult>;
|
|
21
|
-
}>;
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import { HerokuApiClient } from '@heroku/heroku-fetch';
|
|
2
|
-
import createDebug from 'debug';
|
|
3
|
-
import { extendResource } from '../../core/extend-resource.js';
|
|
4
|
-
const debug = createDebug('heroku:sdk:resources:pipeline-promotion');
|
|
5
|
-
const DEFAULT_INTERVAL_MS = 1000;
|
|
6
|
-
const DEFAULT_RELEASE_STREAM_MAX_ATTEMPTS = 100;
|
|
7
|
-
export async function promotePipeline(ctx, body, options = {}) {
|
|
8
|
-
const { intervalMs = DEFAULT_INTERVAL_MS, onReleaseStream, releaseStreamMaxAttempts = DEFAULT_RELEASE_STREAM_MAX_ATTEMPTS, signal, timeoutMs, } = options;
|
|
9
|
-
debug('promote source=%s targets=%d', body.source?.app?.id ?? '<unknown>', body.targets?.length ?? 0);
|
|
10
|
-
const promotion = await ctx.platform.pipelinePromotion.create(body);
|
|
11
|
-
if (!promotion.id) {
|
|
12
|
-
throw new Error('Pipeline promotion response did not include an id');
|
|
13
|
-
}
|
|
14
|
-
debug('promote created promotion=%s intervalMs=%d timeoutMs=%s', promotion.id, intervalMs, timeoutMs ?? '<none>');
|
|
15
|
-
const deadline = timeoutMs === undefined ? undefined : Date.now() + timeoutMs;
|
|
16
|
-
const streamRunner = onReleaseStream
|
|
17
|
-
? createReleaseStreamRunner(ctx, onReleaseStream, releaseStreamMaxAttempts, intervalMs, signal)
|
|
18
|
-
: undefined;
|
|
19
|
-
let poll = 0;
|
|
20
|
-
while (true) {
|
|
21
|
-
signal?.throwIfAborted();
|
|
22
|
-
poll++;
|
|
23
|
-
// eslint-disable-next-line no-await-in-loop
|
|
24
|
-
const targets = await ctx.platform.pipelinePromotionTarget.list(promotion.id);
|
|
25
|
-
debug('promote poll=%d promotion=%s statuses=%o', poll, promotion.id, targets.map(target => target.status));
|
|
26
|
-
if (targets.every(target => target.status !== 'pending')) {
|
|
27
|
-
debug('promote terminal promotion=%s polls=%d', promotion.id, poll);
|
|
28
|
-
return { promotion, targets };
|
|
29
|
-
}
|
|
30
|
-
// eslint-disable-next-line no-await-in-loop
|
|
31
|
-
await streamRunner?.(targets);
|
|
32
|
-
if (deadline !== undefined && Date.now() >= deadline) {
|
|
33
|
-
throw new Error(`Pipeline promotion ${promotion.id} did not reach a terminal state within ${timeoutMs}ms`);
|
|
34
|
-
}
|
|
35
|
-
// eslint-disable-next-line no-await-in-loop
|
|
36
|
-
await wait(intervalMs, signal);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Returns a function that, on first call where `targets` is a single
|
|
41
|
-
* promotion target with `release.id` and `app.id`, fetches the release's
|
|
42
|
-
* output stream and hands it to `onReleaseStream`. Subsequent calls are
|
|
43
|
-
* no-ops; the stream is one-shot per promotion.
|
|
44
|
-
*/
|
|
45
|
-
function createReleaseStreamRunner(ctx, onReleaseStream, releaseStreamMaxAttempts, intervalMs, signal) {
|
|
46
|
-
let handled = false;
|
|
47
|
-
return async (targets) => {
|
|
48
|
-
if (handled || targets.length !== 1)
|
|
49
|
-
return;
|
|
50
|
-
const target = targets[0];
|
|
51
|
-
if (!target.release?.id || !target.app?.id)
|
|
52
|
-
return;
|
|
53
|
-
handled = true;
|
|
54
|
-
debug('promote release-stream lookup app=%s release=%s', target.app.id, target.release.id);
|
|
55
|
-
const release = await ctx.platform.release.info(target.app.id, target.release.id);
|
|
56
|
-
if (!release.output_stream_url) {
|
|
57
|
-
debug('promote release-stream skipped reason=no-output-stream-url');
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
debug('promote release-stream fetching url=%s maxAttempts=%d', release.output_stream_url, releaseStreamMaxAttempts);
|
|
61
|
-
const stream = await fetchReleaseOutput(release.output_stream_url, releaseStreamMaxAttempts, intervalMs, signal);
|
|
62
|
-
await onReleaseStream({ stream, target });
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
export const pipelinePromotionExtensions = extendResource('platform', 'pipelinePromotion', ctx => ({
|
|
66
|
-
promote: (body, options) => promotePipeline(ctx, body, options),
|
|
67
|
-
}));
|
|
68
|
-
async function fetchReleaseOutput(url, maxAttempts, intervalMs, signal) {
|
|
69
|
-
const parsed = new URL(url);
|
|
70
|
-
const path = `${parsed.pathname}${parsed.search}`;
|
|
71
|
-
// The release output URL points at a third-party stream host (e.g. busl).
|
|
72
|
-
// Use a custom-service client so the platform prefixUrl and bearer token
|
|
73
|
-
// don't leak across origins.
|
|
74
|
-
const buslClient = new HerokuApiClient({
|
|
75
|
-
baseUrl: parsed.origin,
|
|
76
|
-
service: 'custom',
|
|
77
|
-
token: '',
|
|
78
|
-
});
|
|
79
|
-
let attempt = 0;
|
|
80
|
-
while (true) {
|
|
81
|
-
signal?.throwIfAborted();
|
|
82
|
-
attempt++;
|
|
83
|
-
let response;
|
|
84
|
-
try {
|
|
85
|
-
// eslint-disable-next-line no-await-in-loop
|
|
86
|
-
response = await buslClient.stream(path);
|
|
87
|
-
}
|
|
88
|
-
catch {
|
|
89
|
-
// Treat HTTP errors thrown by the api-client as a retryable miss.
|
|
90
|
-
response = undefined;
|
|
91
|
-
}
|
|
92
|
-
if (response?.ok && response.body) {
|
|
93
|
-
debug('release-stream attempt=%d status=%d ok stream=true', attempt, response.status);
|
|
94
|
-
return response.body;
|
|
95
|
-
}
|
|
96
|
-
debug('release-stream attempt=%d status=%s ok=false', attempt, response?.status ?? '<error>');
|
|
97
|
-
if (response?.body) {
|
|
98
|
-
// Drain so the connection can be reused.
|
|
99
|
-
// eslint-disable-next-line no-await-in-loop
|
|
100
|
-
await response.body.cancel().catch(() => { });
|
|
101
|
-
}
|
|
102
|
-
if (attempt >= maxAttempts) {
|
|
103
|
-
throw new Error('stream release output not available');
|
|
104
|
-
}
|
|
105
|
-
// eslint-disable-next-line no-await-in-loop
|
|
106
|
-
await wait(intervalMs, signal);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
function wait(ms, signal) {
|
|
110
|
-
return new Promise((resolve, reject) => {
|
|
111
|
-
function onAbort() {
|
|
112
|
-
clearTimeout(timer);
|
|
113
|
-
reject(signal.reason ?? new Error('Aborted'));
|
|
114
|
-
}
|
|
115
|
-
const timer = setTimeout(() => {
|
|
116
|
-
signal?.removeEventListener('abort', onAbort);
|
|
117
|
-
resolve();
|
|
118
|
-
}, ms);
|
|
119
|
-
if (signal) {
|
|
120
|
-
if (signal.aborted) {
|
|
121
|
-
clearTimeout(timer);
|
|
122
|
-
reject(signal.reason ?? new Error('Aborted'));
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
signal.addEventListener('abort', onAbort, { once: true });
|
|
126
|
-
}
|
|
127
|
-
});
|
|
128
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import type { Pipeline } from '@heroku/types/3.sdk';
|
|
2
|
-
import { type HerokuApiClientOptions } from '@heroku/heroku-fetch';
|
|
3
|
-
import type { ResourceCtx } from '../../core/extend-resource.js';
|
|
4
|
-
export type ResolvePipelineOptions = {
|
|
5
|
-
clientOptions?: HerokuApiClientOptions;
|
|
6
|
-
signal?: AbortSignal;
|
|
7
|
-
};
|
|
8
|
-
export declare class PipelineNotFoundError extends Error {
|
|
9
|
-
readonly identity: string;
|
|
10
|
-
readonly id = "not_found";
|
|
11
|
-
readonly statusCode = 404;
|
|
12
|
-
constructor(identity: string);
|
|
13
|
-
get body(): {
|
|
14
|
-
id: string;
|
|
15
|
-
message: string;
|
|
16
|
-
resource: string;
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
export declare class PipelineAmbiguousError extends Error {
|
|
20
|
-
readonly identity: string;
|
|
21
|
-
readonly matches: Pipeline[];
|
|
22
|
-
readonly id = "multiple_matches";
|
|
23
|
-
readonly statusCode = 422;
|
|
24
|
-
constructor(identity: string, matches: Pipeline[]);
|
|
25
|
-
get body(): {
|
|
26
|
-
id: string;
|
|
27
|
-
message: string;
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Resolve a Pipeline by identity.
|
|
32
|
-
*
|
|
33
|
-
* The identity may be:
|
|
34
|
-
* - a UUID — fetched directly via `pipeline.info`.
|
|
35
|
-
* - a name — looked up via `GET /pipelines?eq[name]=<name>`. Pipeline
|
|
36
|
-
* names are not unique platform-wide, so a name lookup may return
|
|
37
|
-
* zero, one, or many matches.
|
|
38
|
-
*
|
|
39
|
-
* The name path uses a raw `HerokuApiClient` because the route
|
|
40
|
-
* registry's `pipeline.list` doesn't accept query params, and we don't
|
|
41
|
-
* want to bake those params into the routes-generated client (where
|
|
42
|
-
* they would leak across unrelated calls).
|
|
43
|
-
*
|
|
44
|
-
* Errors:
|
|
45
|
-
* - throws `PipelineNotFoundError` when no pipeline matches the name.
|
|
46
|
-
* - throws `PipelineAmbiguousError` when more than one does. Callers
|
|
47
|
-
* that want to prompt the user should catch this and inspect
|
|
48
|
-
* `error.matches`.
|
|
49
|
-
*/
|
|
50
|
-
export declare function resolvePipeline(ctx: Pick<ResourceCtx, 'platform'>, identity: string, options?: ResolvePipelineOptions): Promise<Pipeline>;
|
|
51
|
-
export declare const pipelineExtensions: import("../../index.js").ResourceExtension<"platform", "pipeline", {
|
|
52
|
-
resolve: (identity: string, options?: ResolvePipelineOptions) => Promise<Pipeline>;
|
|
53
|
-
}>;
|