From a201502c3794a9c59090813381693a8f530747f4 Mon Sep 17 00:00:00 2001 From: Vi Date: Mon, 2 Nov 2026 14:43:19 -0800 Subject: [PATCH] feat(connect): connect Blacksmith from the CLI Co-Authored-By: Claude Opus 5.5 --- src/commands/integration/connect.ts | 32 ++++++++++++++++++++++- test/integration-connect-category.test.ts | 8 +++--- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/src/commands/integration/connect.ts b/src/commands/integration/connect.ts index 93ce5d3..4d0b6c9 100644 --- a/src/commands/integration/connect.ts +++ b/src/commands/integration/connect.ts @@ -55,6 +55,7 @@ type ConnectableType = | 'braintrust' | 'incident_io' | 'depot' + | 'blacksmith' | 'mixpanel' | 'amplitude' | 'posthog' @@ -104,6 +105,7 @@ const TYPE_OPTIONS: Array<{ value: ConnectableType; label: string; hint: string; { value: 'braintrust', label: 'Braintrust', hint: 'service token ยท LLM observability', category: 'observability' }, { value: 'incident_io', label: 'incident.io', hint: 'API key', category: 'incident-management' }, { value: 'depot', label: 'Depot', hint: 'organization 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' }, @@ -1339,6 +1341,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 === 'amplitude') { let apiKey = ''; let secretKey = ''; @@ -1518,7 +1546,8 @@ export const integrationConnectCommand: Command = { { flag: '--organization-api-key ', description: 'Organization API key (Logfire only, pylf_...)', type: 'string' }, { flag: '--management-api-key-id ', description: 'Management API key ID (Honeycomb)', type: 'string' }, { flag: '--management-api-key-secret ', description: 'Management API key secret (Honeycomb)', type: 'string' }, - { flag: '--api-token ', description: 'API token (Axiom / Better Stack global token / Depot organization token)', type: 'string' }, + { flag: '--api-token ', description: 'API token (Axiom / Better Stack global token / Depot or Blacksmith organization token)', type: 'string' }, + { flag: '--organization ', description: 'GitHub organization login whose jobs run on Blacksmith (Blacksmith only)', type: 'string' }, { flag: '--stack-url ', description: 'Grafana Cloud stack URL, e.g. https://mystack.grafana.net', type: 'string' }, { flag: '--service-account-token ', description: 'Service account token (Grafana only, glsa_...)', type: 'string' }, { flag: '--team-name ', description: 'Team name (Better Stack only)', type: 'string' }, @@ -1567,6 +1596,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 amplitude --api-key ... --secret-key ...', 'polylane integration connect --type posthog', 'polylane integration connect --type cursor --api-key crsr_...', diff --git a/test/integration-connect-category.test.ts b/test/integration-connect-category.test.ts index f5877fc..62226dc 100644 --- a/test/integration-connect-category.test.ts +++ b/test/integration-connect-category.test.ts @@ -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, 26); + assert.equal(all.length, 27); }); it('narrows to exactly the observability integrations', () => { @@ -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']); + assert.deepEqual(typeOptionsForCategory('ci-cd').map((o) => o.value), ['depot', 'blacksmith']); }); it('narrows to exactly the issue trackers', () => { @@ -95,9 +95,9 @@ describe('typeOptionsForCategory', () => { describe('resolveTypeOptions', () => { it('lets --type win over the filter', () => { - assert.equal(resolveTypeOptions('observability', true).length, 26); + assert.equal(resolveTypeOptions('observability', true).length, 27); assert.equal(resolveTypeOptions('observability', false).length, 11); - assert.equal(resolveTypeOptions(undefined, false).length, 26); + assert.equal(resolveTypeOptions(undefined, false).length, 27); }); it('rejects an unknown category even when --type is present', () => {