From a86942c29e192df0159d8342df727d85a3fe936b Mon Sep 17 00:00:00 2001 From: Chap Ambrose Date: Wed, 7 Jan 2026 20:11:35 +0000 Subject: [PATCH 1/2] try repos-api for pipelines:connect --- .../cli/src/commands/pipelines/connect.ts | 21 ++++-- packages/cli/src/lib/api.ts | 1 + packages/cli/src/lib/pipelines/repos-api.ts | 18 +++++ .../commands/pipelines/connect.unit.test.ts | 71 +++++++++++++++++++ 4 files changed, 104 insertions(+), 7 deletions(-) create mode 100644 packages/cli/src/lib/pipelines/repos-api.ts diff --git a/packages/cli/src/commands/pipelines/connect.ts b/packages/cli/src/commands/pipelines/connect.ts index 0eb660e8fe..bc1d2a4e1d 100644 --- a/packages/cli/src/commands/pipelines/connect.ts +++ b/packages/cli/src/commands/pipelines/connect.ts @@ -4,6 +4,7 @@ import {Args, ux} from '@oclif/core' import {getPipeline} from '../../lib/api' import GitHubAPI from '../../lib/pipelines/github-api' import KolkrabbiAPI from '../../lib/pipelines/kolkrabbi-api' +import {createPipelineRepository} from '../../lib/pipelines/repos-api' import getGitHubToken from '../../lib/pipelines/setup/get-github-token' import getNameAndRepo from '../../lib/pipelines/setup/get-name-and-repo' import getRepo from '../../lib/pipelines/setup/get-repo' @@ -47,20 +48,26 @@ export default class Connect extends Command { return } - const kolkrabbi = new KolkrabbiAPI(this.config.userAgent, () => this.heroku.auth) - const github = new GitHubAPI(this.config.userAgent, await getGitHubToken(kolkrabbi)) - const { name: pipelineName, repo: repoName, } = await getNameAndRepo(combinedInputs) - - const repo = await getRepo(github, repoName) - const pipeline = await getPipeline(this.heroku, pipelineName) ux.action.start('Linking to repo') - await kolkrabbi.createPipelineRepository(pipeline.body.id, repo.id) + + // Attempt repos-api connection first + try { + const repoUrl = repoName.includes('.') ? repoName : `https://github.com/${repoName}` + await createPipelineRepository(this.heroku, pipeline.body.id!, repoUrl) + // Fallback to kolkrabbi + } catch (error) { + const kolkrabbi = new KolkrabbiAPI(this.config.userAgent, () => this.heroku.auth) + const github = new GitHubAPI(this.config.userAgent, await getGitHubToken(kolkrabbi)) + const repo = await getRepo(github, repoName) + await kolkrabbi.createPipelineRepository(pipeline.body.id, repo.id) + } + ux.action.stop() } } diff --git a/packages/cli/src/lib/api.ts b/packages/cli/src/lib/api.ts index 4e704e4d5b..c0745b54c2 100644 --- a/packages/cli/src/lib/api.ts +++ b/packages/cli/src/lib/api.ts @@ -6,6 +6,7 @@ export const V3_HEADER = 'application/vnd.heroku+json; version=3' export const SDK_HEADER = 'application/vnd.heroku+json; version=3.sdk' export const FILTERS_HEADER = `${V3_HEADER}.filters` export const PIPELINES_HEADER = `${V3_HEADER}.pipelines` +export const REPOSITORIES_HEADER = `${V3_HEADER}.repositories-api` const CI_HEADER = `${V3_HEADER}.ci` export type Owner = Pick | Pick diff --git a/packages/cli/src/lib/pipelines/repos-api.ts b/packages/cli/src/lib/pipelines/repos-api.ts new file mode 100644 index 0000000000..545977a8cf --- /dev/null +++ b/packages/cli/src/lib/pipelines/repos-api.ts @@ -0,0 +1,18 @@ +import {APIClient} from '@heroku-cli/command' +import {REPOSITORIES_HEADER} from '../api' + +export interface PipelineRepository { + repository?: { + id?: string + url?: string + } +} + +export function createPipelineRepository(heroku: APIClient, pipelineId: string, repoUrl: string) { + return heroku.request(`/pipelines/${pipelineId}/repo`, { + method: 'POST', + headers: {Accept: REPOSITORIES_HEADER}, + body: {repo_url: repoUrl}, + }) +} + diff --git a/packages/cli/test/unit/commands/pipelines/connect.unit.test.ts b/packages/cli/test/unit/commands/pipelines/connect.unit.test.ts index 1f92f3c60b..672fa41883 100644 --- a/packages/cli/test/unit/commands/pipelines/connect.unit.test.ts +++ b/packages/cli/test/unit/commands/pipelines/connect.unit.test.ts @@ -5,6 +5,14 @@ describe('pipelines:connect', function () { test .stderr() .stdout() + .nock('https://api.heroku.com', api => { + const pipeline = { + id: '123', + name: 'my-pipeline', + } + api.get(`/pipelines/${pipeline.name}`).reply(200, pipeline) + api.post(`/pipelines/${pipeline.id}/repo`).reply(422, {}) + }) .nock('https://kolkrabbi.heroku.com', kolkrabbi => { kolkrabbi.get('/account/github/token').reply(401, {}) }) @@ -59,6 +67,14 @@ describe('pipelines:connect', function () { describe('with an account connected to GitHub experiencing request failures', function () { test + .nock('https://api.heroku.com', api => { + const pipeline = { + id: '123', + name: 'my-pipeline', + } + api.get(`/pipelines/${pipeline.name}`).reply(200, pipeline) + api.post(`/pipelines/${pipeline.id}/repo`).reply(422, {}) + }) .nock('https://kolkrabbi.heroku.com', kolkrabbi => { const kolkrabbiAccount = { github: { @@ -81,4 +97,59 @@ describe('pipelines:connect', function () { }) .it('shows an error if GitHub request fails') }) + + describe('with an account connected via repos-api', function () { + test + .nock('https://api.heroku.com', api => { + const pipeline = { + id: '123', + name: 'my-pipeline', + } + api.get(`/pipelines/${pipeline.name}`).reply(200, pipeline) + api.post(`/pipelines/${pipeline.id}/repo`).reply(201, {}) + }) + .stderr() + .stdout() + .command(['pipelines:connect', 'my-pipeline', '--repo=github.com/my-org/my-repo']) + .it('shows success', ctx => { + expect(ctx.stderr).to.include('Linking to repo...') + expect(ctx.stdout).to.equal('') + }) + }) + + describe('repos-api fallback to kolkrabbi', function () { + test + .nock('https://api.heroku.com', api => { + const pipeline = { + id: '123', + name: 'my-pipeline', + } + api.get(`/pipelines/${pipeline.name}`).reply(200, pipeline) + api.post(`/pipelines/${pipeline.id}/repo`).reply(422, {}) + }) + .nock('https://kolkrabbi.heroku.com', kolkrabbi => { + const kolkrabbiAccount = { + github: { + token: '123-abc', + }, + } + kolkrabbi.get('/account/github/token').reply(200, kolkrabbiAccount) + kolkrabbi.post('/pipelines/123/repository').reply(201, {}) + }) + .nock('https://api.github.com', github => { + const repo = { + id: 1235, + default_branch: 'main', + name: 'my-org/my-repo', + } + github.get(`/repos/${repo.name}`).reply(200, repo) + }) + .stderr() + .stdout() + .command(['pipelines:connect', 'my-pipeline', '--repo=my-org/my-repo']) + .it('shows success', ctx => { + expect(ctx.stderr).to.include('Linking to repo...') + expect(ctx.stdout).to.equal('') + }) + }) }) From 593bc4744996079ada1f02bcefd2c20cc38edc63 Mon Sep 17 00:00:00 2001 From: Chap Ambrose Date: Wed, 7 Jan 2026 21:10:40 +0000 Subject: [PATCH 2/2] cleanup --- packages/cli/src/commands/pipelines/connect.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/cli/src/commands/pipelines/connect.ts b/packages/cli/src/commands/pipelines/connect.ts index bc1d2a4e1d..45288825bb 100644 --- a/packages/cli/src/commands/pipelines/connect.ts +++ b/packages/cli/src/commands/pipelines/connect.ts @@ -55,13 +55,12 @@ export default class Connect extends Command { const pipeline = await getPipeline(this.heroku, pipelineName) ux.action.start('Linking to repo') - // Attempt repos-api connection first try { const repoUrl = repoName.includes('.') ? repoName : `https://github.com/${repoName}` await createPipelineRepository(this.heroku, pipeline.body.id!, repoUrl) // Fallback to kolkrabbi - } catch (error) { + } catch { const kolkrabbi = new KolkrabbiAPI(this.config.userAgent, () => this.heroku.auth) const github = new GitHubAPI(this.config.userAgent, await getGitHubToken(kolkrabbi)) const repo = await getRepo(github, repoName)