Skip to content

Commit d1cead5

Browse files
brettfoCopilot
andcommitted
Add dependabot CLI execution mode behind experiment flag
Introduces a new code path that uses the dependabot CLI binary instead of the Docker-based updater orchestration when the dependabot-use-cli experiment is enabled. Key changes: - Download the latest CLI release from GitHub Releases, resolving the version dynamically via the GitHub API - Build a YAML job file from the job details and credentials - Pass --api-url, --updater-image, and --proxy-image flags to the CLI - Set JOB_TOKEN, DEPENDABOT_JOB_ID, and LOCAL_GITHUB_ACCESS_TOKEN in the CLI process environment for proxy auth and image pulls - Log CLI stderr as info (not warning) - Skip the CLI path when the job environment is ghes; allow it for dotcom and proxima environments Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent b0148c5 commit d1cead5

14 files changed

Lines changed: 7195 additions & 2313 deletions

__tests__/cli-runner.test.ts

Lines changed: 548 additions & 0 deletions
Large diffs are not rendered by default.

__tests__/main.test.ts

Lines changed: 267 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,20 @@ import {Updater} from '../src/updater'
1212
import {ImageService, MetricReporter} from '../src/image-service'
1313
import {updaterImageName} from '../src/docker-tags'
1414
import * as inputs from '../src/inputs'
15-
import {run, credentialsFromEnv, getPackagesCredential} from '../src/main'
15+
import {
16+
run,
17+
credentialsFromEnv,
18+
getPackagesCredential,
19+
isCliAllowedForEnvironment
20+
} from '../src/main'
21+
import * as cliRunner from '../src/cli-runner'
1622

1723
import {eventFixturePath} from './helpers'
1824

1925
// We do not need to build actual containers or run updates for this test.
2026
jest.mock('../src/image-service')
2127
jest.mock('../src/updater')
28+
jest.mock('../src/cli-runner')
2229

2330
describe('run', () => {
2431
let context: Context
@@ -1219,3 +1226,262 @@ describe('getPackagesCredential', () => {
12191226
})
12201227
})
12211228
})
1229+
1230+
describe('run with dependabot-use-cli experiment', () => {
1231+
let context: Context
1232+
let markJobAsProcessedSpy: any
1233+
let reportJobErrorSpy: any
1234+
1235+
beforeEach(async () => {
1236+
process.env.GITHUB_EVENT_PATH = eventFixturePath('default')
1237+
process.env.GITHUB_EVENT_NAME = 'dynamic'
1238+
process.env.GITHUB_ACTOR = 'dependabot[bot]'
1239+
process.env.GITHUB_TRIGGERING_ACTOR = 'dependabot[bot]'
1240+
1241+
process.env.GITHUB_SERVER_URL = 'https://github.com'
1242+
process.env.GITHUB_REPOSITORY = 'foo/bar'
1243+
1244+
process.env.GITHUB_DEPENDABOT_JOB_TOKEN = 'xxx'
1245+
process.env.GITHUB_DEPENDABOT_CRED_TOKEN = 'yyy'
1246+
1247+
jest.spyOn(inputs, 'getJobParameters').mockReturnValue(
1248+
new inputs.JobParameters(
1249+
1,
1250+
'xxx',
1251+
'yyy',
1252+
'https://example.com',
1253+
'172.17.0.1',
1254+
'image/name:tag',
1255+
'dotcom'
1256+
)
1257+
)
1258+
1259+
markJobAsProcessedSpy = jest.spyOn(
1260+
ApiClient.prototype,
1261+
'markJobAsProcessed'
1262+
)
1263+
markJobAsProcessedSpy.mockImplementation(jest.fn())
1264+
reportJobErrorSpy = jest.spyOn(ApiClient.prototype, 'reportJobError')
1265+
reportJobErrorSpy.mockImplementation(jest.fn())
1266+
jest
1267+
.spyOn(ApiClient.prototype, 'getCredentials')
1268+
.mockImplementation(jest.fn(async () => []))
1269+
1270+
jest.spyOn(core, 'info').mockImplementation(jest.fn())
1271+
jest.spyOn(core, 'warning').mockImplementation(jest.fn())
1272+
jest.spyOn(core, 'setFailed').mockImplementation(jest.fn())
1273+
jest.spyOn(core, 'startGroup').mockImplementation(jest.fn())
1274+
jest.spyOn(core, 'endGroup').mockImplementation(jest.fn())
1275+
})
1276+
1277+
afterEach(async () => {
1278+
jest.clearAllMocks()
1279+
})
1280+
1281+
describe('when the dependabot-use-cli experiment is enabled', () => {
1282+
beforeEach(() => {
1283+
jest.spyOn(ApiClient.prototype, 'getJobDetails').mockImplementationOnce(
1284+
jest.fn(async () => {
1285+
return {
1286+
'package-manager': 'npm_and_yarn',
1287+
experiments: {'dependabot-use-cli': true}
1288+
} as unknown as JobDetails
1289+
})
1290+
)
1291+
jest.spyOn(cliRunner, 'runDependabotCLI').mockResolvedValue(undefined)
1292+
context = new Context()
1293+
})
1294+
1295+
test('it uses the CLI runner with hyphenated experiment name', async () => {
1296+
await run(context)
1297+
1298+
expect(cliRunner.runDependabotCLI).toHaveBeenCalled()
1299+
expect(Updater).not.toHaveBeenCalled()
1300+
})
1301+
})
1302+
1303+
describe('when the dependabot-use-cli experiment is not set', () => {
1304+
beforeEach(() => {
1305+
jest.spyOn(ApiClient.prototype, 'getJobDetails').mockImplementationOnce(
1306+
jest.fn(async () => {
1307+
return {
1308+
'package-manager': 'npm_and_yarn',
1309+
experiments: {}
1310+
} as unknown as JobDetails
1311+
})
1312+
)
1313+
context = new Context()
1314+
})
1315+
1316+
test('it follows the standard Docker-based flow', async () => {
1317+
await run(context)
1318+
1319+
expect(cliRunner.runDependabotCLI).not.toHaveBeenCalled()
1320+
})
1321+
})
1322+
1323+
describe('when the CLI runner fails', () => {
1324+
beforeEach(() => {
1325+
jest.spyOn(ApiClient.prototype, 'getJobDetails').mockImplementationOnce(
1326+
jest.fn(async () => {
1327+
return {
1328+
'package-manager': 'npm_and_yarn',
1329+
experiments: {'dependabot-use-cli': true}
1330+
} as unknown as JobDetails
1331+
})
1332+
)
1333+
jest
1334+
.spyOn(cliRunner, 'runDependabotCLI')
1335+
.mockRejectedValue(new Error('CLI failed'))
1336+
context = new Context()
1337+
})
1338+
1339+
test('it reports the error to the API', async () => {
1340+
await run(context)
1341+
1342+
expect(reportJobErrorSpy).toHaveBeenCalledWith(
1343+
expect.objectContaining({
1344+
'error-type': 'actions_workflow_updater',
1345+
'error-details': {'action-error': 'CLI failed'}
1346+
})
1347+
)
1348+
expect(markJobAsProcessedSpy).toHaveBeenCalled()
1349+
})
1350+
1351+
test('it sets the action as failed', async () => {
1352+
await run(context)
1353+
1354+
expect(core.setFailed).toHaveBeenCalledWith(
1355+
expect.stringContaining(
1356+
'Dependabot CLI encountered an error performing the update'
1357+
)
1358+
)
1359+
})
1360+
})
1361+
1362+
describe('when the CLI runner fails with a non-Error value', () => {
1363+
beforeEach(() => {
1364+
jest.spyOn(ApiClient.prototype, 'getJobDetails').mockImplementationOnce(
1365+
jest.fn(async () => {
1366+
return {
1367+
'package-manager': 'npm_and_yarn',
1368+
experiments: {'dependabot-use-cli': true}
1369+
} as unknown as JobDetails
1370+
})
1371+
)
1372+
jest.spyOn(cliRunner, 'runDependabotCLI').mockRejectedValue('CLI failed')
1373+
context = new Context()
1374+
})
1375+
1376+
test('it reports the error to the API', async () => {
1377+
await run(context)
1378+
1379+
expect(reportJobErrorSpy).toHaveBeenCalledWith(
1380+
expect.objectContaining({
1381+
'error-type': 'actions_workflow_updater',
1382+
'error-details': {'action-error': 'CLI failed'}
1383+
})
1384+
)
1385+
expect(markJobAsProcessedSpy).toHaveBeenCalled()
1386+
})
1387+
1388+
test('it sets the action as failed', async () => {
1389+
await run(context)
1390+
1391+
expect(core.setFailed).toHaveBeenCalledWith(
1392+
expect.stringContaining(
1393+
'Dependabot CLI encountered an error performing the update'
1394+
)
1395+
)
1396+
})
1397+
})
1398+
1399+
describe('when running on GitHub Enterprise Server', () => {
1400+
beforeEach(() => {
1401+
jest.spyOn(inputs, 'getJobParameters').mockReturnValue(
1402+
new inputs.JobParameters(
1403+
1,
1404+
'xxx',
1405+
'yyy',
1406+
'https://example.com',
1407+
'172.17.0.1',
1408+
'image/name:tag',
1409+
'ghes'
1410+
)
1411+
)
1412+
jest.spyOn(ApiClient.prototype, 'getJobDetails').mockImplementationOnce(
1413+
jest.fn(async () => {
1414+
return {
1415+
'package-manager': 'npm_and_yarn',
1416+
experiments: {'dependabot-use-cli': true}
1417+
} as unknown as JobDetails
1418+
})
1419+
)
1420+
context = new Context()
1421+
})
1422+
1423+
test('it does not use the CLI runner', async () => {
1424+
await run(context)
1425+
1426+
expect(cliRunner.runDependabotCLI).not.toHaveBeenCalled()
1427+
})
1428+
})
1429+
1430+
describe('when running in proxima environment', () => {
1431+
beforeEach(() => {
1432+
jest.spyOn(inputs, 'getJobParameters').mockReturnValue(
1433+
new inputs.JobParameters(
1434+
1,
1435+
'xxx',
1436+
'yyy',
1437+
'https://example.com',
1438+
'172.17.0.1',
1439+
'image/name:tag',
1440+
'proxima'
1441+
)
1442+
)
1443+
jest.spyOn(ApiClient.prototype, 'getJobDetails').mockImplementationOnce(
1444+
jest.fn(async () => {
1445+
return {
1446+
'package-manager': 'npm_and_yarn',
1447+
experiments: {'dependabot-use-cli': true}
1448+
} as unknown as JobDetails
1449+
})
1450+
)
1451+
jest.spyOn(cliRunner, 'runDependabotCLI').mockResolvedValue(undefined)
1452+
context = new Context()
1453+
})
1454+
1455+
test('it uses the CLI runner', async () => {
1456+
await run(context)
1457+
1458+
expect(cliRunner.runDependabotCLI).toHaveBeenCalled()
1459+
})
1460+
})
1461+
})
1462+
1463+
describe('isCliAllowedForEnvironment', () => {
1464+
test('returns true when environment is dotcom', () => {
1465+
expect(isCliAllowedForEnvironment('dotcom')).toBe(true)
1466+
})
1467+
1468+
test('returns true when environment is proxima', () => {
1469+
expect(isCliAllowedForEnvironment('proxima')).toBe(true)
1470+
})
1471+
1472+
test('returns false when environment is ghes', () => {
1473+
expect(isCliAllowedForEnvironment('ghes')).toBe(false)
1474+
})
1475+
1476+
test('returns false when environment is undefined', () => {
1477+
expect(isCliAllowedForEnvironment(undefined)).toBe(false)
1478+
})
1479+
1480+
test('returns false when environment is empty string', () => {
1481+
expect(isCliAllowedForEnvironment('')).toBe(false)
1482+
})
1483+
1484+
test('returns false when environment is unknown', () => {
1485+
expect(isCliAllowedForEnvironment('unknown')).toBe(false)
1486+
})
1487+
})

0 commit comments

Comments
 (0)