From 55013f97c6a8708559c50b6da769bb777c4055f6 Mon Sep 17 00:00:00 2001 From: Johnny Winn Date: Mon, 27 Jul 2026 14:13:10 -0600 Subject: [PATCH] feat(ci): add git repo guard to ci:run and ci:rerun Adds an early inGitRepo() check to ci:run and ci:rerun that throws a clear error message when run outside a git repository, preventing confusing failures from createSourceBlob() silently swallowing git errors. Closes #1761 (W-23597902) Co-Authored-By: Claude Sonnet 4.6 (1M context) --- src/commands/ci/rerun.ts | 6 ++++++ src/commands/ci/run.ts | 5 +++++ test/unit/commands/ci/rerun.unit.test.ts | 21 +++++++++++++++++++++ test/unit/commands/ci/run.unit.test.ts | 21 +++++++++++++++++++++ 4 files changed, 53 insertions(+) diff --git a/src/commands/ci/rerun.ts b/src/commands/ci/rerun.ts index 0a81750633..1b062bc351 100644 --- a/src/commands/ci/rerun.ts +++ b/src/commands/ci/rerun.ts @@ -3,6 +3,7 @@ import * as Heroku from '@heroku-cli/schema' import * as color from '@heroku/heroku-cli-util/color' import {Args, ux} from '@oclif/core' +import {gitService} from '../../lib/ci/git.js' import * as Kolkrabbi from '../../lib/ci/interfaces/kolkrabbi.js' import {getPipeline} from '../../lib/ci/pipelines.js' import {createSourceBlob} from '../../lib/ci/source.js' @@ -25,6 +26,11 @@ export default class CiReRun extends Command { async run() { const {args, flags} = await this.parse(CiReRun) + + if (!gitService.inGitRepo()) { + this.error('Not in a git repository. ci:rerun must be run from within your app\'s git repo.') + } + const pipeline = await getPipeline(flags, this.heroku) let sourceTestRun: Heroku.TestRun diff --git a/src/commands/ci/run.ts b/src/commands/ci/run.ts index 8bbbbe415b..475140dc68 100644 --- a/src/commands/ci/run.ts +++ b/src/commands/ci/run.ts @@ -23,6 +23,11 @@ export default class CiRun extends Command { async run() { const {flags} = await this.parse(CiRun) + + if (!gitService.inGitRepo()) { + this.error('Not in a git repository. ci:run must be run from within your app\'s git repo.') + } + const pipeline = await getPipeline(flags, this.heroku) const commit = await gitService.readCommit('HEAD') diff --git a/test/unit/commands/ci/rerun.unit.test.ts b/test/unit/commands/ci/rerun.unit.test.ts index 4f2ee551c6..d1c4bfc5df 100644 --- a/test/unit/commands/ci/rerun.unit.test.ts +++ b/test/unit/commands/ci/rerun.unit.test.ts @@ -29,6 +29,26 @@ describe('ci:rerun', function () { } }) + describe('when not in a git repository', function () { + let sandbox: ReturnType + + beforeEach(function () { + sandbox = createSandbox() + sandbox.stub(gitService, 'inGitRepo').returns(false as any) + }) + + afterEach(function () { + sandbox.restore() + }) + + it('errors with a clear message', async function () { + const {error} = await runCommand(Cmd, ['--pipeline=my-pipeline']) + expect(error).to.exist + expect(error?.message).to.contain('Not in a git repository') + expect(error?.message).to.contain('ci:rerun must be run from within your app\'s git repo') + }) + }) + describe('when specifying a pipeline', function () { const pipeline = {id: '14402644-c207-43aa-9bc1-974a34914010', name: 'pipeline'} const ghRepository = { @@ -58,6 +78,7 @@ describe('ci:rerun', function () { sandbox = createSandbox() // Stub gitService methods + sandbox.stub(gitService, 'inGitRepo').returns(true) sandbox.stub(gitService, 'githubRepository').resolves({repo: ghRepository.repo, user: ghRepository.user} as any) sandbox.stub(gitService, 'createArchive').resolves('new-archive.tgz') diff --git a/test/unit/commands/ci/run.unit.test.ts b/test/unit/commands/ci/run.unit.test.ts index 3fd046ecc8..b65f4a6a68 100644 --- a/test/unit/commands/ci/run.unit.test.ts +++ b/test/unit/commands/ci/run.unit.test.ts @@ -29,6 +29,26 @@ describe('ci:run', function () { } }) + describe('when not in a git repository', function () { + let sandbox: ReturnType + + beforeEach(function () { + sandbox = createSandbox() + sandbox.stub(gitService, 'inGitRepo').returns(false as any) + }) + + afterEach(function () { + sandbox.restore() + }) + + it('errors with a clear message', async function () { + const {error} = await runCommand(Cmd, ['--pipeline=my-pipeline']) + expect(error).to.exist + expect(error?.message).to.contain('Not in a git repository') + expect(error?.message).to.contain('ci:run must be run from within your app\'s git repo') + }) + }) + describe('when specifying a pipeline', function () { const pipeline = {id: '14402644-c207-43aa-9bc1-974a34914010', name: 'pipeline'} const ghRepository = { @@ -53,6 +73,7 @@ describe('ci:run', function () { sandbox = createSandbox() // Stub gitService methods + sandbox.stub(gitService, 'inGitRepo').returns(true) sandbox.stub(gitService, 'readCommit').resolves({branch: ghRepository.branch, message: `pushed to ${ghRepository.branch}`, ref: ghRepository.ref}) sandbox.stub(gitService, 'githubRepository').resolves({repo: ghRepository.repo, user: ghRepository.user} as any) sandbox.stub(gitService, 'createArchive').resolves('new-archive.tgz')