heroku 11.2.0 → 11.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/CHANGELOG.md CHANGED
@@ -4,6 +4,23 @@ 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.3.0-beta.0](https://github.com/heroku/cli/compare/v11.2.0...v11.3.0-beta.0) (2026-04-14)
8
+
9
+
10
+ ### Features
11
+
12
+ * adds 'data:pg:upgrade:run/wait' commands (W-21304392) ([#3551](https://github.com/heroku/cli/issues/3551)) ([3297c5e](https://github.com/heroku/cli/commit/3297c5e218fe9105cd106000cba030e784d17a82))
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * restore beforeExit handler for version commands and add comprehensive telemetry debug logging ([#3657](https://github.com/heroku/cli/issues/3657)) ([6da79cd](https://github.com/heroku/cli/commit/6da79cd4686133e1ee1f92b59127d8e012739b99))
18
+
19
+
20
+ ### Miscellaneous Chores
21
+
22
+ * remove @oclif/plugin-legacy dependency ([#3659](https://github.com/heroku/cli/issues/3659)) ([2523d48](https://github.com/heroku/cli/commit/2523d481a79f0d26ab8b6897c6b49d3e5713a218))
23
+
7
24
  ## [11.2.0](https://github.com/heroku/cli/compare/v11.1.1...v11.2.0) (2026-04-08)
8
25
 
9
26
 
package/bin/run.js CHANGED
@@ -12,9 +12,11 @@ process.env.HEROKU_UPDATE_INSTRUCTIONS = process.env.HEROKU_UPDATE_INSTRUCTIONS
12
12
  const now = new Date()
13
13
  const cliStartTime = now.getTime()
14
14
 
15
- const {isTelemetryEnabled} = await import('../dist/lib/analytics-telemetry/telemetry-utils.js')
15
+ const {isTelemetryEnabled, getTelemetryDisabledReason, telemetryDebug} = await import('../dist/lib/analytics-telemetry/telemetry-utils.js')
16
+ const enableTelemetry = isTelemetryEnabled()
16
17
 
17
- if (isTelemetryEnabled()) {
18
+ if (enableTelemetry) {
19
+ telemetryDebug('Telemetry enabled: setting up handlers (beforeExit, SIGINT, SIGTERM)')
18
20
  // Dynamically import telemetry modules
19
21
  const {setupTelemetryHandlers} = await import('../dist/lib/analytics-telemetry/worker-client.js')
20
22
  const {computeDuration} = await import('../dist/lib/analytics-telemetry/telemetry-utils.js')
@@ -23,8 +25,11 @@ if (isTelemetryEnabled()) {
23
25
  setupTelemetryHandlers({
24
26
  cliStartTime,
25
27
  computeDuration,
26
- enableTelemetry: isTelemetryEnabled(),
28
+ enableTelemetry,
27
29
  })
30
+ } else {
31
+ const reason = getTelemetryDisabledReason()
32
+ telemetryDebug('Telemetry disabled (%s): skipping telemetry handler setup', reason)
28
33
  }
29
34
 
30
35
  await execute({dir: import.meta.url})
@@ -0,0 +1,15 @@
1
+ import BaseCommand from '../../../../lib/data/baseCommand.js';
2
+ export default class DataPgUpgradeRun extends BaseCommand {
3
+ static args: {
4
+ database: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static examples: string[];
8
+ static flags: {
9
+ app: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
+ confirm: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ remote: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ version: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ };
14
+ run(): Promise<void>;
15
+ }
@@ -0,0 +1,58 @@
1
+ import { flags as Flags } from '@heroku-cli/command';
2
+ import { color, hux, utils } from '@heroku/heroku-cli-util';
3
+ import { Args, ux } from '@oclif/core';
4
+ import tsheredoc from 'tsheredoc';
5
+ import BaseCommand from '../../../../lib/data/baseCommand.js';
6
+ const heredoc = tsheredoc.default;
7
+ export default class DataPgUpgradeRun extends BaseCommand {
8
+ static args = {
9
+ database: Args.string({
10
+ description: 'database name, database attachment name, or related config var on an app',
11
+ required: true,
12
+ }),
13
+ };
14
+ static description = 'upgrade the Postgres version on a Postgres Advanced database';
15
+ static examples = [
16
+ heredoc `
17
+ # Upgrade a Postgres Advanced database to version 17
18
+ ${color.code('<%= config.bin %> <%= command.id %> DATABASE --version 17 --app my-app')}
19
+ `,
20
+ ];
21
+ static flags = {
22
+ app: Flags.app({ required: true }),
23
+ confirm: Flags.string({ char: 'c', description: 'pass in the app name to skip confirmation prompts' }),
24
+ remote: Flags.remote(),
25
+ version: Flags.string({ char: 'v', description: 'Postgres version to upgrade to' }),
26
+ };
27
+ async run() {
28
+ const { args, flags } = await this.parse(DataPgUpgradeRun);
29
+ const { app, confirm, version } = flags;
30
+ const { database } = args;
31
+ const dbResolver = new utils.pg.DatabaseResolver(this.heroku);
32
+ const { addon } = await dbResolver.getAttachment(app, database);
33
+ if (!utils.pg.isAdvancedDatabase(addon)) {
34
+ ux.error('You can only use this command on Advanced-tier databases.\n'
35
+ + `Use ${color.code(`heroku pg:upgrade:run ${addon.name} --app ${app}`)} instead.`);
36
+ }
37
+ const { body: databaseInfo } = await this.dataApi.get(`/data/postgres/v1/${addon.id}/info`);
38
+ const { version: currentVersion } = databaseInfo;
39
+ const newVersion = version ?? 'the latest supported Postgres version';
40
+ await hux.confirmCommand({
41
+ comparison: app,
42
+ confirmation: confirm,
43
+ warningMessage: heredoc(`
44
+ This command immediately upgrades your ${color.datastore(addon.name)} database from ${currentVersion} to ${newVersion}.
45
+ Your database will be unavailable until the upgrade is complete.`),
46
+ });
47
+ try {
48
+ ux.action.start(`Upgrading your ${color.datastore(addon.name)} database from ${currentVersion} to ${newVersion}`);
49
+ await this.dataApi.post(`/data/postgres/v1/${addon.id}/upgrade/run`, { body: { version } });
50
+ ux.action.stop();
51
+ ux.stderr(`Upgrade started. Use ${color.code(`heroku data:pg:upgrade:wait ${addon.name} -a ${app}`)} to monitor progress.`);
52
+ }
53
+ catch (error) {
54
+ ux.action.stop(color.red('!'));
55
+ throw error;
56
+ }
57
+ }
58
+ }
@@ -0,0 +1,6 @@
1
+ import DataPgWait from '../wait.js';
2
+ export default class DataPgUpgradeWait extends DataPgWait {
3
+ static description: string;
4
+ static examples: string[];
5
+ protected classicWaitCommand: string;
6
+ }
@@ -0,0 +1,18 @@
1
+ import * as color from '@heroku/heroku-cli-util/color';
2
+ import tsheredoc from 'tsheredoc';
3
+ import DataPgWait from '../wait.js';
4
+ const heredoc = tsheredoc.default;
5
+ export default class DataPgUpgradeWait extends DataPgWait {
6
+ static description = 'shows status of an upgrade until it\'s complete';
7
+ static examples = [
8
+ heredoc(`
9
+ # Wait for upgrade to complete
10
+ ${color.code('<%= config.bin %> <%= command.id %> DATABASE --app myapp')}
11
+ `),
12
+ heredoc(`
13
+ # Wait with custom polling interval (to avoid rate limiting)
14
+ ${color.code('<%= config.bin %> <%= command.id %> DATABASE --app myapp --wait-interval 10')}
15
+ `),
16
+ ];
17
+ classicWaitCommand = 'pg:upgrade:wait';
18
+ }
@@ -12,6 +12,7 @@ export default class DataPgWait extends BaseCommand {
12
12
  remote: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
13
  'wait-interval': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
14
14
  };
15
+ protected classicWaitCommand: string;
15
16
  notify(...args: Parameters<typeof notify>): Promise<void>;
16
17
  run(): Promise<void>;
17
18
  wait(ms: number): Promise<void>;
@@ -1,5 +1,6 @@
1
- import { color, utils } from '@heroku/heroku-cli-util';
2
1
  import { flags as Flags } from '@heroku-cli/command';
2
+ import * as color from '@heroku/heroku-cli-util/color';
3
+ import { DatabaseResolver, isAdvancedDatabase } from '@heroku/heroku-cli-util/utils';
3
4
  import { Args, ux } from '@oclif/core';
4
5
  import tsheredoc from 'tsheredoc';
5
6
  import BaseCommand from '../../../lib/data/baseCommand.js';
@@ -16,7 +17,7 @@ export default class DataPgWait extends BaseCommand {
16
17
  static examples = [
17
18
  heredoc(`
18
19
  # Wait for database to be available
19
- ${color.command('heroku data:pg:wait DATABASE --app myapp')}
20
+ ${color.code('<%= config.bin %> <%= command.id %> DATABASE --app myapp')}
20
21
  `),
21
22
  ];
22
23
  static flags = {
@@ -31,6 +32,7 @@ export default class DataPgWait extends BaseCommand {
31
32
  min: 1,
32
33
  }),
33
34
  };
35
+ classicWaitCommand = 'pg:wait';
34
36
  async notify(...args) {
35
37
  return notify(...args);
36
38
  }
@@ -38,13 +40,13 @@ export default class DataPgWait extends BaseCommand {
38
40
  const { args, flags } = await this.parse(DataPgWait);
39
41
  const { database } = args;
40
42
  const { app, 'no-notify': noNotify, 'wait-interval': waitInterval } = flags;
41
- const databaseResolver = new utils.pg.DatabaseResolver(this.heroku);
43
+ const databaseResolver = new DatabaseResolver(this.heroku);
42
44
  const db = await databaseResolver.getAttachment(app, database);
43
45
  const { addon } = db;
44
- if (!utils.pg.isAdvancedDatabase(addon)) {
46
+ if (!isAdvancedDatabase(addon)) {
45
47
  ux.error(heredoc `
46
48
  You can only use this command on Advanced-tier databases.
47
- Run ${color.code(`heroku pg:wait ${addon.name} -a ${app}`)} instead.`);
49
+ Use ${color.code(`heroku ${this.classicWaitCommand} ${addon.name} -a ${app}`)} instead.`);
48
50
  }
49
51
  await this.waitFor(addon, waitInterval || 5, noNotify);
50
52
  }
@@ -1,9 +1,12 @@
1
1
  const performance_analytics = async function () {
2
- const { isTelemetryEnabled } = await import('../../lib/analytics-telemetry/telemetry-utils.js');
2
+ const { isTelemetryEnabled, getTelemetryDisabledReason, telemetryDebug } = await import('../../lib/analytics-telemetry/telemetry-utils.js');
3
3
  // Use the consolidated telemetry check
4
4
  if (!isTelemetryEnabled()) {
5
+ const reason = getTelemetryDisabledReason();
6
+ telemetryDebug('Telemetry disabled (%s): skipping command_not_found hook, not setting up telemetry for invalid command', reason);
5
7
  return;
6
8
  }
9
+ telemetryDebug('Telemetry enabled: command_not_found hook setting up telemetry for invalid command');
7
10
  const { telemetryManager } = await import('../../lib/analytics-telemetry/telemetry-manager.js');
8
11
  const globalAny = global;
9
12
  globalAny.cliTelemetry = telemetryManager.reportCmdNotFound(this.config);
@@ -31,11 +31,14 @@ const finallyHook = async function (options) {
31
31
  if (isUserError(options.error)) {
32
32
  return;
33
33
  }
34
- const { isTelemetryEnabled, spawnTelemetryWorker } = await import('../../lib/analytics-telemetry/telemetry-utils.js');
34
+ const { isTelemetryEnabled, getTelemetryDisabledReason, spawnTelemetryWorker, telemetryDebug } = await import('../../lib/analytics-telemetry/telemetry-utils.js');
35
35
  // Use the consolidated telemetry check
36
36
  if (!isTelemetryEnabled()) {
37
+ const reason = getTelemetryDisabledReason();
38
+ telemetryDebug('Telemetry disabled (%s): skipping finally hook, not sending error: %s', reason, options.error.message);
37
39
  return;
38
40
  }
41
+ telemetryDebug('Telemetry enabled: finally hook spawning worker to send error: %s', options.error.message);
39
42
  // Spawn background process to send error without blocking
40
43
  spawnTelemetryWorker(options.error);
41
44
  };
@@ -1,9 +1,12 @@
1
1
  const performance_analytics = async function (options) {
2
- const { isTelemetryEnabled } = await import('../../lib/analytics-telemetry/telemetry-utils.js');
2
+ const { isTelemetryEnabled, getTelemetryDisabledReason, telemetryDebug } = await import('../../lib/analytics-telemetry/telemetry-utils.js');
3
3
  // Use the consolidated telemetry check
4
4
  if (!isTelemetryEnabled()) {
5
+ const reason = getTelemetryDisabledReason();
6
+ telemetryDebug('Telemetry disabled (%s): skipping init hook, not setting up telemetry object', reason);
5
7
  return;
6
8
  }
9
+ telemetryDebug('Telemetry enabled: init hook setting up telemetry object for command');
7
10
  const { telemetryManager } = await import('../../lib/analytics-telemetry/telemetry-manager.js');
8
11
  const globalAny = global;
9
12
  globalAny.cliTelemetry = telemetryManager.setupTelemetry(this.config, options);
@@ -3,15 +3,20 @@ const performance_analytics = async function () {
3
3
  if (!globalAny.cliTelemetry) {
4
4
  return;
5
5
  }
6
- const { computeDuration, isTelemetryEnabled, spawnTelemetryWorker } = await import('../../lib/analytics-telemetry/telemetry-utils.js');
6
+ const { computeDuration, isTelemetryEnabled, getTelemetryDisabledReason, spawnTelemetryWorker, telemetryDebug } = await import('../../lib/analytics-telemetry/telemetry-utils.js');
7
7
  // Use the consolidated telemetry check
8
8
  if (!isTelemetryEnabled()) {
9
+ const reason = getTelemetryDisabledReason();
10
+ telemetryDebug('Telemetry disabled (%s): skipping postrun hook, not sending telemetry for command: %s', reason, globalAny.cliTelemetry.command);
9
11
  return;
10
12
  }
13
+ telemetryDebug('Telemetry enabled: postrun hook spawning worker to send telemetry for command: %s', globalAny.cliTelemetry.command);
11
14
  const cmdStartTime = globalAny.cliTelemetry.commandRunDuration;
12
15
  globalAny.cliTelemetry.commandRunDuration = computeDuration(cmdStartTime);
13
16
  globalAny.cliTelemetry.lifecycleHookCompletion.postrun = true;
14
17
  // Spawn background process to send telemetry without blocking
15
18
  spawnTelemetryWorker(globalAny.cliTelemetry);
19
+ // Mark telemetry as sent to prevent duplicate sends from beforeExit handler
20
+ globalAny.telemetrySent = true;
16
21
  };
17
22
  export default performance_analytics;
@@ -1,9 +1,12 @@
1
1
  const analytics = async function (options) {
2
- const { isTelemetryEnabled, spawnTelemetryWorker } = await import('../../lib/analytics-telemetry/telemetry-utils.js');
2
+ const { isTelemetryEnabled, getTelemetryDisabledReason, spawnTelemetryWorker, telemetryDebug } = await import('../../lib/analytics-telemetry/telemetry-utils.js');
3
3
  // Use the consolidated telemetry check
4
4
  if (!isTelemetryEnabled()) {
5
+ const reason = getTelemetryDisabledReason();
6
+ telemetryDebug('Telemetry disabled (%s): skipping prerun hook, not sending Herokulytics for command: %s', reason, options.Command.id);
5
7
  return;
6
8
  }
9
+ telemetryDebug('Telemetry enabled: prerun hook spawning worker to send Herokulytics for command: %s', options.Command.id);
7
10
  const { telemetryManager } = await import('../../lib/analytics-telemetry/telemetry-manager.js');
8
11
  const globalAny = global;
9
12
  // Only setup telemetry if not already initialized (avoid overwriting init hook data)
@@ -40,7 +40,6 @@ class TelemetryManager {
40
40
  */
41
41
  async sendTelemetry(currentTelemetry) {
42
42
  if (!isTelemetryEnabled()) {
43
- telemetryDebug('Telemetry disabled, skipping send');
44
43
  return;
45
44
  }
46
45
  const { backboardOtelClient, sentryClient } = await this.getClients();
@@ -48,12 +48,17 @@ export interface Telemetry {
48
48
  export type TelemetryData = CLIError | Telemetry;
49
49
  export interface TelemetryGlobal {
50
50
  cliTelemetry?: Telemetry;
51
+ telemetrySent?: boolean;
51
52
  }
52
53
  export type WorkerData = HerokulyticsData | TelemetryData;
53
54
  /**
54
55
  * Compute duration from a start time to now
55
56
  */
56
57
  export declare function computeDuration(cmdStartTime: number): number;
58
+ /**
59
+ * Get the reason why telemetry is disabled (for logging purposes)
60
+ */
61
+ export declare function getTelemetryDisabledReason(): null | string;
57
62
  /**
58
63
  * Get authentication token, cached to avoid recreating Config/APIClient
59
64
  * Lazy-loads @heroku-cli/command and @oclif/core/config to avoid loading them during CLI init
@@ -65,6 +70,7 @@ export declare function getToken(): Promise<string | undefined>;
65
70
  export declare function getVersion(): string;
66
71
  /**
67
72
  * Check if telemetry is enabled based on environment variables
73
+ * Returns both the enabled status and a reason string for logging
68
74
  */
69
75
  export declare function isTelemetryEnabled(): boolean;
70
76
  /**
@@ -21,6 +21,21 @@ export function computeDuration(cmdStartTime) {
21
21
  const cmdFinishTime = now.getTime();
22
22
  return cmdFinishTime - cmdStartTime;
23
23
  }
24
+ /**
25
+ * Get the reason why telemetry is disabled (for logging purposes)
26
+ */
27
+ export function getTelemetryDisabledReason() {
28
+ if (process.env.DISABLE_TELEMETRY === 'true') {
29
+ return 'DISABLE_TELEMETRY=true';
30
+ }
31
+ if (process.platform === 'win32' && process.env.ENABLE_WINDOWS_TELEMETRY !== 'true') {
32
+ return 'Windows platform requires ENABLE_WINDOWS_TELEMETRY=true';
33
+ }
34
+ if (process.env.IS_HEROKU_TEST_ENV === 'true') {
35
+ return 'IS_HEROKU_TEST_ENV=true';
36
+ }
37
+ return null;
38
+ }
24
39
  /**
25
40
  * Get authentication token, cached to avoid recreating Config/APIClient
26
41
  * Lazy-loads @heroku-cli/command and @oclif/core/config to avoid loading them during CLI init
@@ -53,14 +68,18 @@ export function getVersion() {
53
68
  }
54
69
  /**
55
70
  * Check if telemetry is enabled based on environment variables
71
+ * Returns both the enabled status and a reason string for logging
56
72
  */
57
73
  export function isTelemetryEnabled() {
58
- if (process.env.DISABLE_TELEMETRY === 'true')
74
+ if (process.env.DISABLE_TELEMETRY === 'true') {
59
75
  return false;
60
- if (process.platform === 'win32' && process.env.ENABLE_WINDOWS_TELEMETRY !== 'true')
76
+ }
77
+ if (process.platform === 'win32' && process.env.ENABLE_WINDOWS_TELEMETRY !== 'true') {
61
78
  return false;
62
- if (process.env.IS_HEROKU_TEST_ENV === 'true')
79
+ }
80
+ if (process.env.IS_HEROKU_TEST_ENV === 'true') {
63
81
  return false;
82
+ }
64
83
  return true;
65
84
  }
66
85
  /**
@@ -1,6 +1,7 @@
1
1
  import { TelemetryGlobal } from './telemetry-utils.js';
2
2
  declare global {
3
3
  var cliTelemetry: TelemetryGlobal['cliTelemetry'];
4
+ var telemetrySent: TelemetryGlobal['telemetrySent'];
4
5
  }
5
6
  interface SetupTelemetryOptions {
6
7
  cliStartTime: number;
@@ -8,9 +9,10 @@ interface SetupTelemetryOptions {
8
9
  enableTelemetry: boolean;
9
10
  }
10
11
  /**
11
- * Setup telemetry handlers for signal handlers
12
- * Note: Normal command completion telemetry is handled by the postrun hook.
13
- * This only handles SIGINT/SIGTERM cases where hooks don't run.
12
+ * Setup telemetry handlers for beforeExit and signal handlers
13
+ * - beforeExit: Fallback for commands where postrun hook doesn't run (e.g., version, --help flags)
14
+ * - postrun hook: Handles regular commands (sets telemetrySent flag to prevent duplicates)
15
+ * - SIGINT/SIGTERM: Handles interrupted commands
14
16
  */
15
17
  export declare function setupTelemetryHandlers(options: SetupTelemetryOptions): void;
16
18
  export {};
@@ -1,16 +1,30 @@
1
1
  /* eslint-disable n/no-process-exit */
2
- import { spawnTelemetryWorker } from './telemetry-utils.js';
2
+ /* eslint-disable no-var */
3
+ import { spawnTelemetryWorker, telemetryDebug, } from './telemetry-utils.js';
3
4
  /**
4
- * Setup telemetry handlers for signal handlers
5
- * Note: Normal command completion telemetry is handled by the postrun hook.
6
- * This only handles SIGINT/SIGTERM cases where hooks don't run.
5
+ * Setup telemetry handlers for beforeExit and signal handlers
6
+ * - beforeExit: Fallback for commands where postrun hook doesn't run (e.g., version, --help flags)
7
+ * - postrun hook: Handles regular commands (sets telemetrySent flag to prevent duplicates)
8
+ * - SIGINT/SIGTERM: Handles interrupted commands
7
9
  */
8
10
  export function setupTelemetryHandlers(options) {
9
11
  const { cliStartTime, computeDuration, enableTelemetry } = options;
10
12
  if (!enableTelemetry)
11
13
  return;
12
- // Note: beforeExit handler removed to avoid duplicate telemetry sends.
13
- // The postrun hook now handles normal command completion telemetry.
14
+ // Fallback handler for commands that don't trigger postrun hook
15
+ // (e.g., version, --version, --help flags handled by oclif)
16
+ process.once('beforeExit', code => {
17
+ // Only send if telemetry wasn't already sent by postrun hook
18
+ if (global.cliTelemetry && !global.telemetrySent) {
19
+ telemetryDebug('Telemetry enabled: beforeExit spawning worker to send telemetry for command: %s (postrun did not run)', global.cliTelemetry.command);
20
+ const cmdStartTime = global.cliTelemetry.commandRunDuration;
21
+ global.cliTelemetry.commandRunDuration = computeDuration(cmdStartTime);
22
+ global.cliTelemetry.exitCode = code;
23
+ global.cliTelemetry.cliRunDuration = computeDuration(cliStartTime);
24
+ spawnTelemetryWorker(global.cliTelemetry);
25
+ global.telemetrySent = true;
26
+ }
27
+ });
14
28
  process.on('SIGINT', () => {
15
29
  // Spawn background process to send telemetry
16
30
  const error = Object.assign(new Error('Received SIGINT'), {