Skip to content

Commit ef0a274

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 ef0a274

10 files changed

Lines changed: 4969 additions & 22 deletions

File tree

__tests__/cli-runner.test.ts

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

__tests__/main.test.ts

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

0 commit comments

Comments
 (0)