heroku 11.5.0 → 11.6.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.
@@ -1,4 +1,4 @@
1
- import { vars } from '@heroku-cli/command';
1
+ import { APIClient } from '@heroku-cli/command';
2
2
  import { HTTP } from '@heroku/http-call';
3
3
  import fs from 'fs-extra';
4
4
  import path from 'node:path';
@@ -8,8 +8,8 @@ export default class BackboardHerokulyticsClient {
8
8
  config;
9
9
  http;
10
10
  userConfig;
11
+ heroku;
11
12
  isInitialized = false;
12
- netrc;
13
13
  constructor(config) {
14
14
  this.config = config;
15
15
  this.http = HTTP.create({
@@ -17,26 +17,11 @@ export default class BackboardHerokulyticsClient {
17
17
  });
18
18
  }
19
19
  get authorizationToken() {
20
- return process.env.HEROKU_API_KEY || this.netrcToken;
21
- }
22
- get netrcLogin() {
23
- return this.netrc?.machines[vars.apiHost]?.login;
24
- }
25
- get netrcToken() {
26
- return this.netrc?.machines[vars.apiHost]?.password;
20
+ return this.heroku.auth;
27
21
  }
28
22
  get url() {
29
23
  return process.env.HEROKU_ANALYTICS_URL || 'https://backboard.heroku.com/hamurai';
30
24
  }
31
- get user() {
32
- if (this.usingHerokuAPIKey)
33
- return undefined;
34
- return this.netrcLogin;
35
- }
36
- get usingHerokuAPIKey() {
37
- const k = process.env.HEROKU_API_KEY;
38
- return Boolean(k && k.length > 0);
39
- }
40
25
  async _acAnalytics(id) {
41
26
  if (id === 'autocomplete:options')
42
27
  return 0;
@@ -110,10 +95,8 @@ export default class BackboardHerokulyticsClient {
110
95
  }
111
96
  telemetryDebug('Initializing Herokulytics client...');
112
97
  this.isInitialized = true;
113
- const NetrcModule = await import('netrc-parser');
114
- const NetrcClass = NetrcModule.Netrc || NetrcModule.default.constructor;
115
- this.netrc = new NetrcClass();
116
- await this.netrc.load();
98
+ this.heroku = new APIClient(this.config);
99
+ await this.heroku.getAuth();
117
100
  this.userConfig = new HerokulyticsConfig(this.config);
118
101
  await this.userConfig.init();
119
102
  telemetryDebug('Herokulytics client initialized (install_id: %s)', this.userConfig.install);
@@ -1,12 +1,20 @@
1
+ import cp from 'node:child_process';
1
2
  export default class Git {
3
+ private readonly execFile;
4
+ /** Configures `heroku git:credentials` as a Git credential helper
5
+ * that is URL-scoped to Heroku Git operations only.
6
+ */
7
+ configureCredentialHelper(): Promise<void>;
2
8
  createRemote(remote: string, url: string): Promise<string | null>;
9
+ /** Erases stored credentials for the Heroku Git host */
10
+ eraseCredentials(): Promise<void>;
3
11
  exec(args: string[]): Promise<string>;
4
12
  getBranch(symbolicRef: string): Promise<string>;
5
13
  getCommitTitle(ref: string): Promise<string>;
6
14
  getRef(branch: string): Promise<string>;
7
15
  hasGitRemote(remote: string): Promise<boolean>;
8
16
  httpGitUrl(app: string): string;
9
- inGitRepo(): true | undefined;
17
+ inGitRepo(): boolean;
10
18
  readCommit(commit: string): Promise<{
11
19
  branch: string;
12
20
  message: string;
@@ -14,6 +22,11 @@ export default class Git {
14
22
  }>;
15
23
  remoteFromGitConfig(): Promise<string | void>;
16
24
  remoteUrl(name: string): Promise<string>;
17
- spawn(args: string[]): Promise<unknown>;
25
+ /** Removes `heroku git:credentials` from the global config */
26
+ removeCredentialHelper(): Promise<void>;
27
+ spawn(args: string[], options?: {
28
+ input?: string;
29
+ stdio?: cp.StdioOptions;
30
+ }): Promise<unknown>;
18
31
  url(app: string): string;
19
32
  }
@@ -3,18 +3,39 @@ import { ux } from '@oclif/core/ux';
3
3
  import cp from 'node:child_process';
4
4
  import fs from 'node:fs';
5
5
  import { promisify } from 'node:util';
6
- const execFile = promisify(cp.execFile);
6
+ const execFilePromise = promisify(cp.execFile);
7
7
  import debug from 'debug';
8
8
  const gitDebug = debug('git');
9
9
  export default class Git {
10
+ execFile = execFilePromise;
11
+ /** Configures `heroku git:credentials` as a Git credential helper
12
+ * that is URL-scoped to Heroku Git operations only.
13
+ */
14
+ async configureCredentialHelper() {
15
+ const { httpGitHost } = vars;
16
+ await this.exec([
17
+ 'config',
18
+ '--global',
19
+ `credential.https://${httpGitHost}.helper`,
20
+ '!heroku git:credentials',
21
+ ]);
22
+ }
10
23
  createRemote(remote, url) {
11
24
  return this.hasGitRemote(remote)
12
25
  .then(exists => exists ? null : this.exec(['remote', 'add', remote, url]));
13
26
  }
27
+ /** Erases stored credentials for the Heroku Git host */
28
+ async eraseCredentials() {
29
+ const { httpGitHost } = vars;
30
+ await this.spawn(['credential', 'reject'], {
31
+ input: `protocol=https\nhost=${httpGitHost}\n\n`,
32
+ stdio: ['pipe', 'ignore', 'ignore'],
33
+ });
34
+ }
14
35
  async exec(args) {
15
36
  gitDebug('exec: git %o', args);
16
37
  try {
17
- const { stderr, stdout } = await execFile('git', args);
38
+ const { stderr, stdout } = await this.execFile('git', args);
18
39
  if (stderr)
19
40
  process.stderr.write(stderr);
20
41
  return stdout.trim();
@@ -51,6 +72,7 @@ export default class Git {
51
72
  catch (error) {
52
73
  if (error.code !== 'ENOENT')
53
74
  throw error;
75
+ return false;
54
76
  }
55
77
  }
56
78
  async readCommit(commit) {
@@ -73,13 +95,27 @@ export default class Git {
73
95
  .find(r => r[0] === name)?.[1]
74
96
  ?.split(' ')[0] ?? '';
75
97
  }
76
- spawn(args) {
98
+ /** Removes `heroku git:credentials` from the global config */
99
+ async removeCredentialHelper() {
100
+ const { httpGitHost } = vars;
101
+ await this.exec(['config', '--global', '--unset-all', `credential.https://${httpGitHost}.helper`]);
102
+ }
103
+ spawn(args, options = {}) {
77
104
  return new Promise((resolve, reject) => {
78
105
  gitDebug('spawn: git %o', args);
79
- const s = cp.spawn('git', args, { stdio: [0, 1, 2] });
106
+ const s = cp.spawn('git', args, { stdio: options.stdio ?? [0, 1, 2] });
107
+ if (options.input && s.stdin) {
108
+ s.stdin.write(options.input);
109
+ s.stdin.end();
110
+ }
80
111
  s.on('error', (err) => {
81
112
  if (err.code === 'ENOENT') {
82
- ux.error('Git must be installed to use the Heroku CLI. See instructions here: https://git-scm.com');
113
+ try {
114
+ ux.error('Git must be installed to use the Heroku CLI. See instructions here: https://git-scm.com');
115
+ }
116
+ catch (error) {
117
+ reject(error);
118
+ }
83
119
  }
84
120
  else
85
121
  reject(err);