heroku 11.8.0 → 11.8.1
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/CHANGELOG.md +15 -0
- package/dist/commands/data/pg/migrate.d.ts +1 -0
- package/dist/commands/data/pg/migrate.js +13 -4
- package/dist/commands/pg/calls.js +2 -2
- package/dist/commands/pg/outliers.d.ts +1 -2
- package/dist/commands/pg/outliers.js +6 -22
- package/dist/lib/analytics-telemetry/backboard-herokulytics-client.d.ts +3 -2
- package/dist/lib/analytics-telemetry/telemetry-utils.d.ts +25 -1
- package/dist/lib/analytics-telemetry/telemetry-utils.js +34 -5
- package/dist/lib/analytics-telemetry/telemetry-worker.js +9 -4
- package/dist/lib/pg/extras.d.ts +1 -1
- package/dist/lib/pg/extras.js +9 -7
- package/dist/lib/run/dyno.js +2 -1
- package/npm-shrinkwrap.json +302 -81
- package/oclif.manifest.json +754 -754
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,21 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
6
|
|
|
7
|
+
## [11.8.1](https://github.com/heroku/cli/compare/v11.8.0...v11.8.1) (2026-07-08)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Bug Fixes
|
|
11
|
+
|
|
12
|
+
* consolidate pg_stat_statements handling ([#3799](https://github.com/heroku/cli/issues/3799)) ([e36b494](https://github.com/heroku/cli/commit/e36b49472b67f5fdcaf5332764d996fe1994ecfd))
|
|
13
|
+
* keep process type resolvable with heroku run --exit-code ([#3804](https://github.com/heroku/cli/issues/3804)) ([6383b03](https://github.com/heroku/cli/commit/6383b03e21b4bb088bc39f13fafb8a3c680f53a2))
|
|
14
|
+
* migration method selection navigation bug (W-23306091) ([#3802](https://github.com/heroku/cli/issues/3802)) ([8562909](https://github.com/heroku/cli/commit/85629096f63f5e646975d90475ad18e36c8fb53e))
|
|
15
|
+
* restore Sentry release tag from spawned telemetry worker ([#3801](https://github.com/heroku/cli/issues/3801)) ([407a7e0](https://github.com/heroku/cli/commit/407a7e04901844e91b91aeeea70fbdd5085304a5))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Code Refactoring
|
|
19
|
+
|
|
20
|
+
* trim exit-code command comment ([7a2cfed](https://github.com/heroku/cli/commit/7a2cfed35c292ccd14a476c88c90949ec1c2c798))
|
|
21
|
+
|
|
7
22
|
## [11.8.0](https://github.com/heroku/cli/compare/v11.7.1...v11.8.0) (2026-07-01)
|
|
8
23
|
|
|
9
24
|
|
|
@@ -13,6 +13,7 @@ export default class DataPgMigrate extends BaseCommand {
|
|
|
13
13
|
private appName;
|
|
14
14
|
private classicDatabases;
|
|
15
15
|
private extendedLevelsInfo;
|
|
16
|
+
private methodProvidedViaFlag;
|
|
16
17
|
private migrationTargets;
|
|
17
18
|
private selectedMigrationMethod?;
|
|
18
19
|
createAddon(...args: Parameters<typeof createAddon>): Promise<Heroku.AddOn>;
|
|
@@ -26,6 +26,7 @@ export default class DataPgMigrate extends BaseCommand {
|
|
|
26
26
|
appName;
|
|
27
27
|
classicDatabases = [];
|
|
28
28
|
extendedLevelsInfo;
|
|
29
|
+
methodProvidedViaFlag = false;
|
|
29
30
|
migrationTargets = [];
|
|
30
31
|
selectedMigrationMethod;
|
|
31
32
|
async createAddon(...args) {
|
|
@@ -38,8 +39,10 @@ export default class DataPgMigrate extends BaseCommand {
|
|
|
38
39
|
const { flags } = await this.parse(DataPgMigrate);
|
|
39
40
|
const { app, method } = flags;
|
|
40
41
|
this.appName = app;
|
|
41
|
-
// If --method flag is provided, convert and store
|
|
42
|
+
// If --method flag is provided, convert and store, and record that the method
|
|
43
|
+
// came from the flag so the interactive selection step is skipped throughout.
|
|
42
44
|
if (method !== undefined) {
|
|
45
|
+
this.methodProvidedViaFlag = true;
|
|
43
46
|
this.selectedMigrationMethod = method === 'streaming' ? MigrationMethod.CDC : MigrationMethod.FULL_LOAD;
|
|
44
47
|
}
|
|
45
48
|
ux.stdout(heredoc `
|
|
@@ -146,6 +149,12 @@ export default class DataPgMigrate extends BaseCommand {
|
|
|
146
149
|
}
|
|
147
150
|
}
|
|
148
151
|
async configureMigration() {
|
|
152
|
+
// When the method wasn't provided via the --method flag, clear any method
|
|
153
|
+
// selected during a previous migration in this session so a stale value can
|
|
154
|
+
// never reach the migration request if the interactive step is ever skipped.
|
|
155
|
+
if (!this.methodProvidedViaFlag) {
|
|
156
|
+
this.selectedMigrationMethod = undefined;
|
|
157
|
+
}
|
|
149
158
|
let currentStep = '__select_source';
|
|
150
159
|
let sourceDatabaseId;
|
|
151
160
|
let targetDatabaseId;
|
|
@@ -266,7 +275,7 @@ export default class DataPgMigrate extends BaseCommand {
|
|
|
266
275
|
case '__confirm_migration': {
|
|
267
276
|
const action = await confirmMigration();
|
|
268
277
|
if (action === '__go_back') {
|
|
269
|
-
currentStep = '__select_target';
|
|
278
|
+
currentStep = this.methodProvidedViaFlag ? '__select_target' : '__select_method';
|
|
270
279
|
}
|
|
271
280
|
else if (action === '__confirm') {
|
|
272
281
|
ux.stdout('');
|
|
@@ -308,14 +317,14 @@ export default class DataPgMigrate extends BaseCommand {
|
|
|
308
317
|
if (addon) {
|
|
309
318
|
targetDatabaseId = addon.id;
|
|
310
319
|
targetDatabaseName = addon.name;
|
|
311
|
-
currentStep = this.
|
|
320
|
+
currentStep = this.methodProvidedViaFlag ? '__confirm_migration' : '__select_method';
|
|
312
321
|
}
|
|
313
322
|
else {
|
|
314
323
|
currentStep = '__select_target';
|
|
315
324
|
}
|
|
316
325
|
}
|
|
317
326
|
else {
|
|
318
|
-
currentStep = this.
|
|
327
|
+
currentStep = this.methodProvidedViaFlag ? '__confirm_migration' : '__select_method';
|
|
319
328
|
}
|
|
320
329
|
break;
|
|
321
330
|
}
|
|
@@ -6,7 +6,7 @@ import { ensurePGStatStatement, newBlkTimeFields, newTotalExecTimeField } from '
|
|
|
6
6
|
import { nls } from '../../nls.js';
|
|
7
7
|
const heredoc = tsheredoc.default;
|
|
8
8
|
export async function generateCallsQuery(db, flags) {
|
|
9
|
-
await ensurePGStatStatement(db);
|
|
9
|
+
const schema = await ensurePGStatStatement(db);
|
|
10
10
|
const truncatedQueryString = flags.truncate
|
|
11
11
|
? 'CASE WHEN length(query) <= 40 THEN query ELSE substr(query, 0, 39) || \'…\' END'
|
|
12
12
|
: 'query';
|
|
@@ -21,7 +21,7 @@ to_char((${totalExecTimeField}/sum(${totalExecTimeField}) OVER()) * 100, 'FM90D0
|
|
|
21
21
|
to_char(calls, 'FM999G999G999G990') AS ncalls,
|
|
22
22
|
interval '1 millisecond' * (${blkReadField} + ${blkWriteField}) AS sync_io_time,
|
|
23
23
|
${truncatedQueryString} AS query
|
|
24
|
-
FROM pg_stat_statements WHERE userid = (SELECT usesysid FROM pg_user WHERE usename = current_user LIMIT 1)
|
|
24
|
+
FROM ${schema}.pg_stat_statements WHERE userid = (SELECT usesysid FROM pg_user WHERE usename = current_user LIMIT 1)
|
|
25
25
|
ORDER BY calls DESC
|
|
26
26
|
LIMIT 10
|
|
27
27
|
`.trim();
|
|
@@ -13,7 +13,6 @@ export default class Outliers extends Command {
|
|
|
13
13
|
};
|
|
14
14
|
static topic: string;
|
|
15
15
|
private psqlService;
|
|
16
|
-
protected
|
|
17
|
-
protected outliersQuery(version: string | undefined, limit: number, truncate: boolean): string;
|
|
16
|
+
protected outliersQuery(version: string | undefined, limit: number, truncate: boolean, schema: string): string;
|
|
18
17
|
run(): Promise<void>;
|
|
19
18
|
}
|
|
@@ -2,6 +2,7 @@ import { Command, flags } from '@heroku-cli/command';
|
|
|
2
2
|
import { utils } from '@heroku/heroku-cli-util';
|
|
3
3
|
import { Args, ux } from '@oclif/core';
|
|
4
4
|
import tsheredoc from 'tsheredoc';
|
|
5
|
+
import { ensurePGStatStatement } from '../../lib/pg/extras.js';
|
|
5
6
|
import { fetchVersion } from '../../lib/pg/psql.js';
|
|
6
7
|
import { nls } from '../../nls.js';
|
|
7
8
|
const heredoc = tsheredoc.default;
|
|
@@ -19,24 +20,7 @@ export default class Outliers extends Command {
|
|
|
19
20
|
};
|
|
20
21
|
static topic = 'pg';
|
|
21
22
|
psqlService;
|
|
22
|
-
|
|
23
|
-
const query = heredoc `
|
|
24
|
-
SELECT exists(
|
|
25
|
-
SELECT 1
|
|
26
|
-
FROM pg_extension e
|
|
27
|
-
LEFT JOIN pg_namespace n ON n.oid = e.extnamespace
|
|
28
|
-
WHERE e.extname = 'pg_stat_statements' AND n.nspname IN ('public', 'heroku_ext')
|
|
29
|
-
) AS available;
|
|
30
|
-
`;
|
|
31
|
-
const output = await this.psqlService.execQuery(query);
|
|
32
|
-
if (!output.includes('t')) {
|
|
33
|
-
ux.error(heredoc `
|
|
34
|
-
pg_stat_statements extension need to be installed first.
|
|
35
|
-
You can install it by running: CREATE EXTENSION pg_stat_statements WITH SCHEMA heroku_ext;
|
|
36
|
-
`);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
outliersQuery(version, limit, truncate) {
|
|
23
|
+
outliersQuery(version, limit, truncate, schema) {
|
|
40
24
|
const truncatedQueryString = truncate
|
|
41
25
|
? heredoc `
|
|
42
26
|
CASE WHEN length(query) <= 40 THEN query ELSE substr(query, 0, 39) || '…' END
|
|
@@ -61,7 +45,7 @@ export default class Outliers extends Command {
|
|
|
61
45
|
to_char(calls, 'FM999G999G999G990') AS ncalls,
|
|
62
46
|
interval '1 millisecond' * (${blkReadTimeField} + ${blkWriteTimeField}) AS sync_io_time,
|
|
63
47
|
${truncatedQueryString} AS query
|
|
64
|
-
FROM pg_stat_statements
|
|
48
|
+
FROM ${schema}.pg_stat_statements
|
|
65
49
|
WHERE userid = (
|
|
66
50
|
SELECT usesysid FROM pg_user WHERE usename = current_user LIMIT 1
|
|
67
51
|
)
|
|
@@ -76,11 +60,11 @@ export default class Outliers extends Command {
|
|
|
76
60
|
const db = await dbResolver.getDatabase(app, args.database);
|
|
77
61
|
this.psqlService = new utils.pg.PsqlService(db);
|
|
78
62
|
const version = await fetchVersion(db);
|
|
79
|
-
await
|
|
63
|
+
const schema = await ensurePGStatStatement(db);
|
|
80
64
|
if (reset) {
|
|
81
65
|
const resetFn = utils.pg.isEssentialDatabase(db.attachment.addon) || utils.pg.isAdvancedDatabase(db.attachment.addon)
|
|
82
66
|
? '_heroku.pg_stat_statements_reset()'
|
|
83
|
-
:
|
|
67
|
+
: `${schema}.pg_stat_statements_reset()`;
|
|
84
68
|
await this.psqlService.execQuery(`SELECT ${resetFn};`);
|
|
85
69
|
return;
|
|
86
70
|
}
|
|
@@ -93,7 +77,7 @@ export default class Outliers extends Command {
|
|
|
93
77
|
ux.error(`Cannot parse num param value "${num}" to a number`);
|
|
94
78
|
}
|
|
95
79
|
}
|
|
96
|
-
const query = this.outliersQuery(version, limit, truncate);
|
|
80
|
+
const query = this.outliersQuery(version, limit, truncate, schema);
|
|
97
81
|
const output = await this.psqlService.execQuery(query);
|
|
98
82
|
ux.stdout(output);
|
|
99
83
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { HTTP } from '@heroku/http-call';
|
|
2
|
-
import {
|
|
2
|
+
import { Interfaces } from '@oclif/core';
|
|
3
3
|
import HerokulyticsConfig from './herokulytics-config.js';
|
|
4
|
+
import { HerokulyticsData } from './telemetry-utils.js';
|
|
4
5
|
export interface AnalyticsInterface {
|
|
5
6
|
event: string;
|
|
6
7
|
properties: {
|
|
@@ -20,7 +21,7 @@ export interface AnalyticsInterface {
|
|
|
20
21
|
}
|
|
21
22
|
export interface RecordOpts {
|
|
22
23
|
argv: string[];
|
|
23
|
-
Command: Command
|
|
24
|
+
Command: HerokulyticsData['Command'];
|
|
24
25
|
}
|
|
25
26
|
export default class BackboardHerokulyticsClient {
|
|
26
27
|
config: Interfaces.Config;
|
|
@@ -51,6 +51,18 @@ export interface TelemetryGlobal {
|
|
|
51
51
|
telemetrySent?: boolean;
|
|
52
52
|
}
|
|
53
53
|
export type WorkerData = HerokulyticsData | TelemetryData;
|
|
54
|
+
export interface WorkerEnvelope {
|
|
55
|
+
cliVersion: string;
|
|
56
|
+
payload: SerializedWorkerData;
|
|
57
|
+
}
|
|
58
|
+
export interface SerializedError {
|
|
59
|
+
[key: string]: unknown;
|
|
60
|
+
_type: 'error';
|
|
61
|
+
message: string;
|
|
62
|
+
name: string;
|
|
63
|
+
stack?: string;
|
|
64
|
+
}
|
|
65
|
+
export type SerializedWorkerData = HerokulyticsData | SerializedError | Telemetry;
|
|
54
66
|
/**
|
|
55
67
|
* Compute duration from a start time to now
|
|
56
68
|
*/
|
|
@@ -74,9 +86,21 @@ export declare function getVersion(): string;
|
|
|
74
86
|
*/
|
|
75
87
|
export declare function isTelemetryEnabled(): boolean;
|
|
76
88
|
/**
|
|
77
|
-
* Serialize data for telemetry worker
|
|
89
|
+
* Serialize data for the telemetry worker.
|
|
90
|
+
*
|
|
91
|
+
* Every payload is wrapped in a WorkerEnvelope. The worker relies on this
|
|
92
|
+
* shape unconditionally — see parseWorkerEnvelope.
|
|
78
93
|
*/
|
|
79
94
|
export declare function serializeTelemetryData(data: WorkerData): string;
|
|
95
|
+
/**
|
|
96
|
+
* Parse a worker envelope from stdin. Pure — no side effects.
|
|
97
|
+
*
|
|
98
|
+
* The caller is responsible for applying the envelope's cliVersion via
|
|
99
|
+
* setVersion before any client (Sentry, OTEL) reads getVersion(). Keeping
|
|
100
|
+
* the parse and the state hydration in the caller makes the ordering
|
|
101
|
+
* visible at the call site.
|
|
102
|
+
*/
|
|
103
|
+
export declare function parseWorkerEnvelope(input: string): WorkerEnvelope;
|
|
80
104
|
/**
|
|
81
105
|
* Set CLI version (called once during setup)
|
|
82
106
|
*/
|
|
@@ -83,13 +83,19 @@ export function isTelemetryEnabled() {
|
|
|
83
83
|
return true;
|
|
84
84
|
}
|
|
85
85
|
/**
|
|
86
|
-
* Serialize data for telemetry worker
|
|
86
|
+
* Serialize data for the telemetry worker.
|
|
87
|
+
*
|
|
88
|
+
* Every payload is wrapped in a WorkerEnvelope. The worker relies on this
|
|
89
|
+
* shape unconditionally — see parseWorkerEnvelope.
|
|
87
90
|
*/
|
|
88
91
|
export function serializeTelemetryData(data) {
|
|
89
|
-
|
|
92
|
+
const envelope = { cliVersion: getVersion(), payload: toSerializedPayload(data) };
|
|
93
|
+
return JSON.stringify(envelope);
|
|
94
|
+
}
|
|
95
|
+
function toSerializedPayload(data) {
|
|
90
96
|
if (data instanceof Error) {
|
|
91
97
|
const errorData = data;
|
|
92
|
-
return
|
|
98
|
+
return {
|
|
93
99
|
// Include any other enumerable properties first
|
|
94
100
|
...data,
|
|
95
101
|
// Then override with important properties to ensure they're captured
|
|
@@ -102,9 +108,32 @@ export function serializeTelemetryData(data) {
|
|
|
102
108
|
oclif: errorData.oclif,
|
|
103
109
|
stack: errorData.stack,
|
|
104
110
|
statusCode: errorData.statusCode,
|
|
105
|
-
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
// Discriminate the remaining, non-Error variants of WorkerData by _type
|
|
114
|
+
// so a new variant becomes a compile error here rather than silently
|
|
115
|
+
// falling through.
|
|
116
|
+
switch (data._type) {
|
|
117
|
+
case 'herokulytics':
|
|
118
|
+
case 'otel': {
|
|
119
|
+
return data;
|
|
120
|
+
}
|
|
121
|
+
default: {
|
|
122
|
+
const _exhaustive = data;
|
|
123
|
+
return _exhaustive;
|
|
124
|
+
}
|
|
106
125
|
}
|
|
107
|
-
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Parse a worker envelope from stdin. Pure — no side effects.
|
|
129
|
+
*
|
|
130
|
+
* The caller is responsible for applying the envelope's cliVersion via
|
|
131
|
+
* setVersion before any client (Sentry, OTEL) reads getVersion(). Keeping
|
|
132
|
+
* the parse and the state hydration in the caller makes the ordering
|
|
133
|
+
* visible at the call site.
|
|
134
|
+
*/
|
|
135
|
+
export function parseWorkerEnvelope(input) {
|
|
136
|
+
return JSON.parse(input);
|
|
108
137
|
}
|
|
109
138
|
/**
|
|
110
139
|
* Set CLI version (called once during setup)
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* This runs as a separate process to avoid blocking the main CLI
|
|
5
5
|
*/
|
|
6
6
|
import { telemetryManager } from './telemetry-manager.js';
|
|
7
|
-
import { telemetryDebug } from './telemetry-utils.js';
|
|
7
|
+
import { parseWorkerEnvelope, setVersion, telemetryDebug, } from './telemetry-utils.js';
|
|
8
8
|
// Set maximum lifetime for worker process (10 seconds)
|
|
9
9
|
// This ensures the worker never hangs indefinitely due to network issues or other failures
|
|
10
10
|
const MAX_WORKER_LIFETIME_MS = 10_000;
|
|
@@ -46,7 +46,13 @@ process.stdin.on('data', chunk => {
|
|
|
46
46
|
});
|
|
47
47
|
process.stdin.on('end', async () => {
|
|
48
48
|
try {
|
|
49
|
-
const
|
|
49
|
+
const envelope = parseWorkerEnvelope(inputData);
|
|
50
|
+
// Restore the CLI version in this worker's module scope so any client
|
|
51
|
+
// that lazily reads getVersion() (Sentry.init, OTEL tracer) sees the
|
|
52
|
+
// real version instead of 'unknown'. Must happen before the first
|
|
53
|
+
// client import below.
|
|
54
|
+
setVersion(envelope.cliVersion);
|
|
55
|
+
const parsed = envelope.payload;
|
|
50
56
|
// Check if this is Herokulytics data using explicit type discriminator
|
|
51
57
|
if (parsed._type === 'herokulytics') {
|
|
52
58
|
// Handle Herokulytics data
|
|
@@ -55,8 +61,7 @@ process.stdin.on('end', async () => {
|
|
|
55
61
|
// Recreate Config from serialized data
|
|
56
62
|
const config = await Config.load();
|
|
57
63
|
const client = new BackboardHerokulyticsClient(config);
|
|
58
|
-
|
|
59
|
-
await client.send(parsed);
|
|
64
|
+
await client.send({ argv: parsed.argv, Command: parsed.Command });
|
|
60
65
|
exitWorker(0);
|
|
61
66
|
return;
|
|
62
67
|
}
|
package/dist/lib/pg/extras.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { pg } from '@heroku/heroku-cli-util';
|
|
|
3
3
|
interface Plan {
|
|
4
4
|
plan: Heroku.AddOn['plan'];
|
|
5
5
|
}
|
|
6
|
-
export declare function ensurePGStatStatement(db: pg.ConnectionDetails): Promise<
|
|
6
|
+
export declare function ensurePGStatStatement(db: pg.ConnectionDetails): Promise<string>;
|
|
7
7
|
export declare function ensureEssentialTierPlan(db: pg.ConnectionDetails): Promise<void>;
|
|
8
8
|
export declare function essentialNumPlan(a: Plan): boolean;
|
|
9
9
|
export declare function newTotalExecTimeField(db: pg.ConnectionDetails): Promise<boolean>;
|
package/dist/lib/pg/extras.js
CHANGED
|
@@ -2,18 +2,20 @@ import { utils } from '@heroku/heroku-cli-util';
|
|
|
2
2
|
import { ux } from '@oclif/core/ux';
|
|
3
3
|
export async function ensurePGStatStatement(db) {
|
|
4
4
|
const query = `
|
|
5
|
-
SELECT
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
SELECT quote_ident(n.nspname)
|
|
6
|
+
FROM pg_extension e
|
|
7
|
+
JOIN pg_namespace n ON n.oid = e.extnamespace
|
|
8
|
+
WHERE e.extname = 'pg_stat_statements'`;
|
|
9
9
|
const psqlService = new utils.pg.PsqlService(db);
|
|
10
|
-
const output = await psqlService.execQuery(query);
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
const output = await psqlService.execQuery(query, ['-t', '-q']);
|
|
11
|
+
const schema = output.split('\n')[0].trim();
|
|
12
|
+
if (!schema) {
|
|
13
|
+
ux.error(`The pg_stat_statements extension needs to be installed first.
|
|
13
14
|
You can install it by running:
|
|
14
15
|
|
|
15
16
|
CREATE EXTENSION pg_stat_statements;`, { exit: 1 });
|
|
16
17
|
}
|
|
18
|
+
return schema;
|
|
17
19
|
}
|
|
18
20
|
export async function ensureEssentialTierPlan(db) {
|
|
19
21
|
const planName = db.attachment?.addon?.plan?.name;
|
package/dist/lib/run/dyno.js
CHANGED
|
@@ -85,7 +85,8 @@ export default class Dyno extends Duplex {
|
|
|
85
85
|
});
|
|
86
86
|
}
|
|
87
87
|
async _doStart(retries = 2) {
|
|
88
|
-
|
|
88
|
+
// Keep a space before `;` so the first token stays a bare process type the runtime can resolve.
|
|
89
|
+
const command = this.opts['exit-code'] ? `${this.opts.command} ; echo "\uFFFF heroku-command-exit-status: $?"` : this.opts.command;
|
|
89
90
|
try {
|
|
90
91
|
const dyno = await this.heroku.post(this.opts.dyno ? `/apps/${this.opts.app}/dynos/${this.opts.dyno}` : `/apps/${this.opts.app}/dynos`, {
|
|
91
92
|
body: {
|