Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion src/commands/integration/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ type ConnectableType =
| 'incident_io'
| 'depot'
| 'circleci'
| 'blacksmith'
| 'mixpanel'
| 'amplitude'
| 'posthog'
Expand Down Expand Up @@ -106,6 +107,7 @@ const TYPE_OPTIONS: Array<{ value: ConnectableType; label: string; hint: string;
{ value: 'incident_io', label: 'incident.io', hint: 'API key', category: 'incident-management' },
{ value: 'depot', label: 'Depot', hint: 'organization token', category: 'ci-cd' },
{ value: 'circleci', label: 'CircleCI', hint: 'personal API token', category: 'ci-cd' },
{ value: 'blacksmith', label: 'Blacksmith', hint: 'organization token + GitHub organization', category: 'ci-cd' },
{ value: 'mixpanel', label: 'Mixpanel', hint: 'service account + project ID', category: 'product-analytics' },
{ value: 'amplitude', label: 'Amplitude', hint: 'API key + secret key', category: 'product-analytics' },
{ value: 'posthog', label: 'PostHog', hint: "choose one project on PostHog's consent screen", category: 'product-analytics' },
Expand Down Expand Up @@ -1383,6 +1385,32 @@ async function connectWithCredentials(
]);
if (!ok) return BACK;
body = { type: 'depot', workspaceId, apiToken };
} else if (type === 'blacksmith') {
let apiToken = '';
let organization = '';
const ok = await runSteps([
secretStep(
config,
args,
'apiToken',
'--api-token',
{
message: 'Blacksmith organization token',
instructions:
'An organization admin runs these commands. Blacksmith confirms admin access in the browser and prints the token once.\n\n curl -fsSL https://get.blacksmith.sh | sh\n blacksmith org-token create --label polylane',
link: 'https://docs.polylane.com/integrations/blacksmith',
linkLabel: 'Open the Blacksmith guide',
},
(v) => {
apiToken = v;
}
),
textStep(config, args, 'organization', 'GitHub organization login whose jobs run on Blacksmith', '--organization', (v) => {
organization = v;
}),
]);
if (!ok) return BACK;
body = { type: 'blacksmith', workspaceId, apiToken, organization };
} else if (type === 'circleci') {
const organizationId = getArgString(args, 'organizationId')?.trim() || undefined;
let apiKey = '';
Expand Down Expand Up @@ -1588,7 +1616,8 @@ export const integrationConnectCommand: Command = {
{ flag: '--organization-api-key <key>', description: 'Organization API key (Logfire only, pylf_...)', type: 'string' },
{ flag: '--management-api-key-id <id>', description: 'Management API key ID (Honeycomb)', type: 'string' },
{ flag: '--management-api-key-secret <secret>', description: 'Management API key secret (Honeycomb)', type: 'string' },
{ flag: '--api-token <token>', description: 'API token (Axiom / Better Stack global token / Depot organization token)', type: 'string' },
{ flag: '--api-token <token>', description: 'API token (Axiom / Better Stack global token / Depot or Blacksmith organization token)', type: 'string' },
{ flag: '--organization <login>', description: 'GitHub organization login whose jobs run on Blacksmith (Blacksmith only)', type: 'string' },
{ flag: '--stack-url <url>', description: 'Grafana Cloud stack URL, e.g. https://mystack.grafana.net', type: 'string' },
{ flag: '--service-account-token <token>', description: 'Service account token (Grafana only, glsa_...)', type: 'string' },
{ flag: '--team-name <name>', description: 'Team name (Better Stack only)', type: 'string' },
Expand Down Expand Up @@ -1637,6 +1666,7 @@ export const integrationConnectCommand: Command = {
'polylane integration connect --type braintrust --api-key bt-st-...',
'polylane integration connect --type incident_io --api-key ... --escalation-severity critical',
'polylane integration connect --type depot --api-token ...',
'polylane integration connect --type blacksmith --api-token ... --organization acme',
'polylane integration connect --type circleci --api-key ... --organization-id <id>',
'polylane integration connect --type amplitude --api-key ... --secret-key ...',
'polylane integration connect --type posthog',
Expand Down
47 changes: 47 additions & 0 deletions test/integration-connect-blacksmith.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { integrationConnectCommand } from '../src/commands/integration/connect';
import { PolylaneAPI } from '../src/generated/client';
import type { Integration } from '../src/generated/types';
import { CLIError } from '../src/errors/base';
import { ExitCode } from '../src/errors/codes';
import { mockConfig } from './helpers/config';

const config = mockConfig({ workspaceId: 'ws_one', quiet: true });

describe('Blacksmith CLI connect', () => {
it('links the token guide when the token is missing', async () => {
await assert.rejects(
() => integrationConnectCommand.execute(config, {}, { type: 'blacksmith' }),
(err: unknown) => {
assert.ok(err instanceof CLIError);
assert.equal(err.exitCode, ExitCode.USAGE);
assert.match(err.message, /--api-token/);
assert.match(err.hint ?? '', /https:\/\/docs\.polylane\.com\/integrations\/blacksmith/);
return true;
}
);
});

it('names the organization flag when the organization is missing', async () => {
await assert.rejects(
() => integrationConnectCommand.execute(config, {}, { type: 'blacksmith', apiToken: 'blacksmith-token' }),
(err: unknown) => err instanceof CLIError
&& err.exitCode === ExitCode.USAGE
&& err.message.includes('--organization')
);
});

it('connects the selected organization with its token', async (t) => {
const seen: unknown[] = [];
t.mock.method(PolylaneAPI.prototype, 'integrationsConnect', async (body) => {
seen.push(body);
return { id: 'int_blacksmith', type: 'blacksmith', name: 'Acme' } as Integration;
});
t.mock.method(process.stdout, 'write', () => true);
await integrationConnectCommand.execute(config, {}, {
type: 'blacksmith', apiToken: 'blacksmith-token', organization: 'acme',
});
assert.deepEqual(seen, [{ type: 'blacksmith', workspaceId: 'ws_one', apiToken: 'blacksmith-token', organization: 'acme' }]);
});
});
8 changes: 4 additions & 4 deletions test/integration-connect-category.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { isCLIError } from '../src/errors/base';
describe('typeOptionsForCategory', () => {
it('returns every option when no category is given', () => {
const all = typeOptionsForCategory(undefined);
assert.equal(all.length, 27);
assert.equal(all.length, 28);
});

it('narrows to exactly the observability integrations', () => {
Expand Down Expand Up @@ -42,7 +42,7 @@ describe('typeOptionsForCategory', () => {

it('narrows to exactly the incident management and CI/CD integrations', () => {
assert.deepEqual(typeOptionsForCategory('incident-management').map((o) => o.value), ['incident_io']);
assert.deepEqual(typeOptionsForCategory('ci-cd').map((o) => o.value), ['depot', 'circleci']);
assert.deepEqual(typeOptionsForCategory('ci-cd').map((o) => o.value), ['depot', 'circleci', 'blacksmith']);
});

it('narrows to exactly the issue trackers', () => {
Expand Down Expand Up @@ -95,9 +95,9 @@ describe('typeOptionsForCategory', () => {

describe('resolveTypeOptions', () => {
it('lets --type win over the filter', () => {
assert.equal(resolveTypeOptions('observability', true).length, 27);
assert.equal(resolveTypeOptions('observability', true).length, 28);
assert.equal(resolveTypeOptions('observability', false).length, 11);
assert.equal(resolveTypeOptions(undefined, false).length, 27);
assert.equal(resolveTypeOptions(undefined, false).length, 28);
});

it('rejects an unknown category even when --type is present', () => {
Expand Down
Loading