Skip to content

Commit da30ac7

Browse files
committed
chore: fix merge conflicts
1 parent 1155d09 commit da30ac7

8 files changed

Lines changed: 74 additions & 65 deletions

File tree

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,11 @@ Maven and Gradle projects should run an install and build before scanning
6868
## Usage
6969
<!-- usage -->
7070
```sh-session
71-
$ npm install -g @herodevs/cli
71+
$ npm install -g @herodevs/cli@beta
7272
$ hd COMMAND
7373
running command...
7474
$ hd (--version)
75-
@herodevs/cli/2.0.0-beta.14 darwin-arm64 node-v20.19.5
75+
@herodevs/cli/2.0.0-beta.14 darwin-arm64 node-v24.10.0
7676
$ hd --help [COMMAND]
7777
USAGE
7878
$ hd COMMAND
@@ -314,7 +314,7 @@ EXAMPLES
314314
$ hd tracker run -d tracker -f settings.json
315315
```
316316

317-
_See code: [src/commands/tracker/run.ts](https://github.com/herodevs/cli/blob/v2.0.0-beta.14/src/commands/tracker/run.ts)_
317+
_See code: [src/commands/tracker/run.ts](https://github.com/herodevs/cli/blob/main/src/commands/tracker/run.ts)_
318318

319319
## `hd update [CHANNEL]`
320320

src/api/user-setup.client.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import { getGraphQLErrors } from './graphql-errors.ts';
1010

1111
const USER_SETUP_MAX_ATTEMPTS = 3;
1212
const USER_SETUP_RETRY_DELAY_MS = 500;
13+
const USER_FACING_SERVER_ERROR = 'Please contact your administrator.';
14+
const SERVER_ERROR_CODES = ['INTERNAL_SERVER_ERROR', 'SERVER_ERROR', 'SERVICE_UNAVAILABLE'];
1315

1416
type UserSetupStatusResponse = {
1517
eol?: {
@@ -39,6 +41,10 @@ export async function getUserSetupStatus(): Promise<boolean> {
3941
if (res?.error || errors?.length) {
4042
debugLogger('Error returned from userSetupStatus query: %o', res.error || errors);
4143
if (errors?.length) {
44+
const rawCode = (errors[0]?.extensions as { code?: string })?.code;
45+
if (rawCode && SERVER_ERROR_CODES.includes(rawCode)) {
46+
throw new Error(USER_FACING_SERVER_ERROR);
47+
}
4248
const code = extractErrorCode(errors);
4349
const message = errors[0].message ?? 'Failed to check user setup status';
4450
if (code) {
@@ -66,6 +72,10 @@ export async function completeUserSetup(): Promise<boolean> {
6672
if (res?.error || errors?.length) {
6773
debugLogger('Error returned from completeUserSetup mutation: %o', res.error || errors);
6874
if (errors?.length) {
75+
const rawCode = (errors[0]?.extensions as { code?: string })?.code;
76+
if (rawCode && SERVER_ERROR_CODES.includes(rawCode)) {
77+
throw new Error(USER_FACING_SERVER_ERROR);
78+
}
6979
const code = extractErrorCode(errors);
7080
const message = errors[0].message ?? 'Failed to complete user setup';
7181
if (code) {

src/commands/auth-ci/provision.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { Command, Flags } from '@oclif/core';
2+
import { provisionCIToken } from '../../api/ci-token.client.ts';
3+
import { requireAccessToken } from '../../service/auth.svc.ts';
4+
import { saveCIOrgId, saveCIToken } from '../../service/ci-token.svc.ts';
5+
import { getErrorMessage } from '../../service/log.svc.ts';
6+
7+
export default class AuthCiProvision extends Command {
8+
static override description = 'Provision a CI/CD long-lived refresh token for headless auth';
9+
10+
static override flags = {
11+
orgId: Flags.integer({
12+
description: 'Organization ID for the CI token (required)',
13+
required: true,
14+
aliases: ['org-id'],
15+
}),
16+
};
17+
18+
async run() {
19+
const { flags } = await this.parse(AuthCiProvision);
20+
const orgId = flags.orgId;
21+
if (orgId === undefined) {
22+
this.error('--org-id is required');
23+
}
24+
25+
try {
26+
await requireAccessToken();
27+
} catch (error) {
28+
this.error(`Must be logged in to provision CI token. Run 'hd auth login' first. ${getErrorMessage(error)}`);
29+
}
30+
31+
try {
32+
const result = await provisionCIToken({ orgId });
33+
const refreshToken = result.refresh_token;
34+
saveCIToken(refreshToken);
35+
saveCIOrgId(orgId);
36+
this.log('CI token provisioned and saved locally.');
37+
this.log(refreshToken);
38+
} catch (error) {
39+
this.error(`CI token provisioning failed. ${getErrorMessage(error)}`);
40+
}
41+
}
42+
}

src/config/constants.ts

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,6 @@ export const DEFAULT_DATE_COMMIT_MONTH_FORMAT = 'MMMM yyyy';
1515
export const ENABLE_AUTH = false;
1616
export const ENABLE_USER_SETUP = false;
1717

18-
const toBoolean = (value: string | undefined): boolean | undefined => {
19-
if (value === 'true') return true;
20-
if (value === 'false') return false;
21-
return undefined;
22-
};
23-
24-
const toPositiveInt = (value: string | undefined): number | undefined => {
25-
if (value === undefined || value === '') return undefined;
26-
const n = Number.parseInt(value, 10);
27-
return Number.isInteger(n) && n >= 1 ? n : undefined;
28-
};
29-
3018
// Trackers - Constants
3119
export const DEFAULT_TRACKER_RUN_DATA_FILE = 'data.json';
3220
export const TRACKER_GIT_OUTPUT_FORMAT = `"${['%H', '%an', '%ad'].join('|')}"`;
@@ -43,6 +31,15 @@ if (parsedPageSize > 0) {
4331
pageSize = parsedPageSize;
4432
}
4533

34+
const enableAuthEnv = process.env.ENABLE_AUTH;
35+
const enableAuth = enableAuthEnv === 'true' ? true : enableAuthEnv === 'false' ? false : ENABLE_AUTH;
36+
const enableUserSetupEnv = process.env.ENABLE_USER_SETUP;
37+
const enableUserSetup =
38+
enableUserSetupEnv === 'true' ? true : enableUserSetupEnv === 'false' ? false : ENABLE_USER_SETUP;
39+
const orgIdEnv = process.env.HD_ORG_ID?.trim();
40+
const orgIdParsed = orgIdEnv ? Number.parseInt(orgIdEnv, 10) : NaN;
41+
const orgIdFromEnv = Number.isInteger(orgIdParsed) && orgIdParsed >= 1 ? orgIdParsed : undefined;
42+
4643
export const config = {
4744
eolReportUrl: process.env.EOL_REPORT_URL || EOL_REPORT_URL,
4845
graphqlHost: process.env.GRAPHQL_HOST || GRAPHQL_HOST,
@@ -52,10 +49,10 @@ export const config = {
5249
analyticsUrl: process.env.ANALYTICS_URL || ANALYTICS_URL,
5350
concurrentPageRequests,
5451
pageSize,
55-
enableAuth: toBoolean(process.env.ENABLE_AUTH) ?? ENABLE_AUTH,
56-
enableUserSetup: toBoolean(process.env.ENABLE_USER_SETUP) ?? ENABLE_USER_SETUP,
52+
enableAuth,
53+
enableUserSetup,
5754
ciTokenFromEnv: process.env.HD_AUTH_TOKEN?.trim() || undefined,
58-
orgIdFromEnv: toPositiveInt(process.env.HD_ORG_ID),
55+
orgIdFromEnv,
5956
accessTokenFromEnv: process.env.HD_ACCESS_TOKEN?.trim() || undefined,
6057
};
6158

test/api/nes.client.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { CreateEolReportInput } from '@herodevs/eol-shared';
22
import { vi } from 'vitest';
3+
34
vi.mock('../../src/config/constants.ts', async (importOriginal) => {
45
const actual = await importOriginal<typeof import('../../src/config/constants.ts')>();
56
return {

test/api/user-setup.client.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ describe('user-setup.client', () => {
5858
{ message: 'Internal server error', extensions: { code: 'INTERNAL_SERVER_ERROR' } },
5959
]);
6060

61-
await expect(ensureUserSetup()).rejects.toThrow('Internal server error');
61+
await expect(ensureUserSetup()).rejects.toThrow('Please contact your administrator.');
6262
expect(fetchMock.getCalls()).toHaveLength(3);
6363
});
6464
});

test/commands/auth-ci/login.test.ts

Lines changed: 1 addition & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { type Mock, vi } from 'vitest';
33
const { mockConfig } = vi.hoisted(() => ({
44
mockConfig: {
55
orgIdFromEnv: undefined as number | undefined,
6-
accessTokenFromEnv: undefined as string | undefined,
76
ciTokenFromEnv: undefined as string | undefined,
87
},
98
}));
@@ -17,29 +16,20 @@ vi.mock('../../../src/api/ci-token.client.ts', () => ({
1716
exchangeCITokenForAccess: vi.fn(),
1817
}));
1918

20-
vi.mock('../../../src/service/auth-token.svc.ts', () => ({
21-
__esModule: true,
22-
getStoredTokens: vi.fn(),
23-
isAccessTokenExpired: vi.fn(),
24-
}));
25-
2619
vi.mock('../../../src/service/ci-token.svc.ts', () => ({
2720
__esModule: true,
2821
getCIToken: vi.fn(),
2922
getCIOrgId: vi.fn(),
30-
saveCIToken: vi.fn(),
3123
}));
3224

3325
import { exchangeCITokenForAccess } from '../../../src/api/ci-token.client.ts';
3426
import AuthCiLogin from '../../../src/commands/auth-ci/login.ts';
35-
import { isAccessTokenExpired } from '../../../src/service/auth-token.svc.ts';
36-
import { getCIOrgId, getCIToken, saveCIToken } from '../../../src/service/ci-token.svc.ts';
27+
import { getCIOrgId, getCIToken } from '../../../src/service/ci-token.svc.ts';
3728

3829
describe('AuthCiLogin command', () => {
3930
beforeEach(() => {
4031
vi.resetAllMocks();
4132
mockConfig.orgIdFromEnv = undefined;
42-
mockConfig.accessTokenFromEnv = undefined;
4333
mockConfig.ciTokenFromEnv = undefined;
4434
});
4535

@@ -60,7 +50,6 @@ describe('AuthCiLogin command', () => {
6050
expect(exchangeCITokenForAccess).toHaveBeenCalledWith({
6151
refreshToken: 'ci-refresh-token',
6252
orgId: 42,
63-
optionalAccessToken: undefined,
6453
});
6554
});
6655

@@ -79,7 +68,6 @@ describe('AuthCiLogin command', () => {
7968

8069
expect(logSpy).toHaveBeenCalledWith('export HD_ACCESS_TOKEN="new-access-token"');
8170
expect(logSpy).toHaveBeenCalledWith('export HD_AUTH_TOKEN="rotated-refresh-token"');
82-
expect(saveCIToken).toHaveBeenCalledWith('rotated-refresh-token');
8371
});
8472

8573
it('uses orgIdFromEnv when set', async () => {
@@ -99,28 +87,6 @@ describe('AuthCiLogin command', () => {
9987
expect(getCIOrgId).not.toHaveBeenCalled();
10088
});
10189

102-
it('passes optionalAccessToken from HD_ACCESS_TOKEN when valid', async () => {
103-
mockConfig.accessTokenFromEnv = 'valid-access-token';
104-
mockConfig.orgIdFromEnv = 42;
105-
(getCIToken as Mock).mockReturnValue('ci-refresh-token');
106-
(isAccessTokenExpired as Mock).mockReturnValue(false);
107-
(exchangeCITokenForAccess as Mock).mockResolvedValue({
108-
accessToken: 'new-access',
109-
refreshToken: 'refresh',
110-
});
111-
const command = new AuthCiLogin([], {} as Record<string, unknown>);
112-
vi.spyOn(command, 'parse').mockResolvedValue({ flags: {}, args: {} } as never);
113-
vi.spyOn(command, 'log').mockImplementation(() => {});
114-
115-
await command.run();
116-
117-
expect(exchangeCITokenForAccess).toHaveBeenCalledWith({
118-
refreshToken: 'ci-refresh-token',
119-
orgId: 42,
120-
optionalAccessToken: 'valid-access-token',
121-
});
122-
});
123-
12490
it('errors when orgId is missing', async () => {
12591
(getCIOrgId as Mock).mockReturnValue(undefined);
12692
(getCIToken as Mock).mockReturnValue('ci-refresh-token');

test/commands/auth-ci/provision.test.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -63,32 +63,25 @@ describe('AuthCiProvision command', () => {
6363
await command.run();
6464

6565
expect(requireAccessToken).toHaveBeenCalled();
66-
expect(provisionCIToken).toHaveBeenCalledWith({
67-
orgId: 123,
68-
previousToken: null,
69-
});
66+
expect(provisionCIToken).toHaveBeenCalledWith({ orgId: 123 });
7067
expect(saveCIToken).toHaveBeenCalledWith('new-ci-refresh-token');
7168
expect(saveCIOrgId).toHaveBeenCalledWith(123);
7269
expect(logSpy).toHaveBeenCalledWith('CI token provisioned and saved locally.');
7370
expect(logSpy).toHaveBeenCalledWith('new-ci-refresh-token');
7471
});
7572

76-
it('passes existing CI token as previousToken when rotating', async () => {
77-
(getCIToken as Mock).mockReturnValue('existing-refresh-token');
73+
it('provisions for different org when logged in', async () => {
7874
(requireAccessToken as Mock).mockResolvedValue('access-token');
7975
(provisionCIToken as Mock).mockResolvedValue({
80-
refresh_token: 'rotated-ci-refresh-token',
76+
refresh_token: 'new-ci-refresh-token',
8177
});
8278
const command = new AuthCiProvision([], {} as Record<string, unknown>);
8379
vi.spyOn(command, 'parse').mockResolvedValue({ flags: { orgId: 456 }, args: {} } as never);
8480
vi.spyOn(command, 'log').mockImplementation(() => {});
8581

8682
await command.run();
8783

88-
expect(provisionCIToken).toHaveBeenCalledWith({
89-
orgId: 456,
90-
previousToken: 'existing-refresh-token',
91-
});
84+
expect(provisionCIToken).toHaveBeenCalledWith({ orgId: 456 });
9285
expect(saveCIOrgId).toHaveBeenCalledWith(456);
9386
});
9487

0 commit comments

Comments
 (0)