Skip to content

Commit

Permalink
Merge pull request #78 from github/brrygrdn/action-reports-failure-to…
Browse files Browse the repository at this point in the history
…-api

Add error reporting to Dependabot API
  • Loading branch information
Jurre authored and GitHub committed Aug 26, 2021
2 parents fac8974 + 4fb3a24 commit 3daf83f
Show file tree
Hide file tree
Showing 5 changed files with 375 additions and 23 deletions.
21 changes: 21 additions & 0 deletions __fixtures__/events/default.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"inputs": {
"jobID": "1",
"jobToken": "xxx",
"credentialsToken": "yyy",
"dependabotAPIURL": "http://localhost:9000"
},
"ref": "main",
"repository": {
"owner": {
"login": "dependabot"
},
"name": "dependabot-core"
},
"sender": {
"type": "User"
},
"installation": null,
"organization": null,
"workflow": "--workflow-yaml-goes-here--"
}
12 changes: 11 additions & 1 deletion __tests__/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const removeDanglingUpdaterContainers = async (): Promise<void> => {
await docker.pruneContainers()
}

export const runFakeDependabotApi = async (port: number): Promise<Function> => {
export const runFakeDependabotApi = async (port = 9000): Promise<Function> => {
const server = spawn('node', [
`${path.join(__dirname, 'server/server.js')}`,
`${port}`
Expand All @@ -44,3 +44,13 @@ export const runFakeDependabotApi = async (port: number): Promise<Function> => {
server.kill()
}
}

export const eventFixturePath = (fixtureName: string): string => {
return path.join(
__dirname,
'..',
'__fixtures__',
'events',
`${fixtureName}.json`
)
}
251 changes: 249 additions & 2 deletions __tests__/main.test.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,249 @@
// shows how the runner will run a javascript action with env / stdout protocol
test('test runs', () => {})
import * as core from '@actions/core'
import {Context} from '@actions/github/lib/context'
import {APIClient} from '../src/api-client'
import {Updater} from '../src/updater'
import {ImageService} from '../src/image-service'
import * as inputs from '../src/inputs'
import {run} from '../src/main'

import {eventFixturePath} from './helpers'

// We do not need to build actual containers or run updates for this test.
jest.mock('../src/api-client')
jest.mock('../src/image-service')
jest.mock('../src/updater')

describe('run', () => {
let context: Context

let markJobAsProcessedSpy: any
let reportJobErrorSpy: any

beforeEach(async () => {
markJobAsProcessedSpy = jest.spyOn(
APIClient.prototype,
'markJobAsProcessed'
)
reportJobErrorSpy = jest.spyOn(APIClient.prototype, 'reportJobError')

jest.spyOn(core, 'info').mockImplementation(jest.fn())
jest.spyOn(core, 'setFailed').mockImplementation(jest.fn())
})

afterEach(async () => {
jest.clearAllMocks() // Reset any mocked classes
})

describe('when the run follows the happy path', () => {
beforeAll(() => {
process.env.GITHUB_EVENT_PATH = eventFixturePath('default')
process.env.GITHUB_EVENT_NAME = 'workflow_dispatch'
context = new Context()
})

test('it signs off at completion without any errors', async () => {
await run(context)

expect(core.setFailed).not.toHaveBeenCalled()
expect(core.info).toHaveBeenCalledWith(
expect.stringContaining('🤖 ~fin~')
)
})

test('it defers reporting back to dependabot-api to the updater itself', async () => {
await run(context)

expect(markJobAsProcessedSpy).not.toHaveBeenCalled()
expect(reportJobErrorSpy).not.toHaveBeenCalled()
})
})

describe('when the action is triggered on an unsupported event', () => {
beforeAll(() => {
process.env.GITHUB_EVENT_PATH = eventFixturePath('default')
process.env.GITHUB_EVENT_NAME = 'issue_created'
context = new Context()
})

test('it fails the workflow', async () => {
await run(context)

expect(core.setFailed).not.toHaveBeenCalled()
expect(core.info).toHaveBeenCalledWith(
"Dependabot Updater Action does not support 'issue_created' events."
)
})

test('it does not report this failed run to dependabot-api', async () => {
await run(context)

expect(markJobAsProcessedSpy).not.toHaveBeenCalled()
expect(reportJobErrorSpy).not.toHaveBeenCalled()
})
})

describe('when there is an error retrieving job parameters', () => {
beforeEach(() => {
jest.spyOn(inputs, 'getJobParameters').mockImplementationOnce(
jest.fn(() => {
throw new Error('unexpected error retrieving job params')
})
)

process.env.GITHUB_EVENT_PATH = eventFixturePath('default')
process.env.GITHUB_EVENT_NAME = 'workflow_dispatch'
context = new Context()
})

test('it fails the workflow with the raw error', async () => {
await run(context)

expect(core.setFailed).toHaveBeenCalledWith(
new Error('unexpected error retrieving job params')
)
})

test('it does not inform dependabot-api as it cannot instantiate a client without the params', async () => {
await run(context)

expect(markJobAsProcessedSpy).not.toHaveBeenCalled()
expect(reportJobErrorSpy).not.toHaveBeenCalled()
})
})

describe('when there is an error retrieving job details from DependabotAPI', () => {
beforeEach(() => {
jest
.spyOn(APIClient.prototype, 'getJobDetails')
.mockImplementationOnce(
jest.fn(async () =>
Promise.reject(new Error('error getting job details'))
)
)

process.env.GITHUB_EVENT_PATH = eventFixturePath('default')
process.env.GITHUB_EVENT_NAME = 'workflow_dispatch'
context = new Context()
})

test('it fails the workflow', async () => {
await run(context)

expect(core.setFailed).toHaveBeenCalledWith(
expect.stringContaining('error getting job details')
)
})

test('it relays a failure message to the dependabot service', async () => {
await run(context)

expect(reportJobErrorSpy).toHaveBeenCalledWith({
'error-type': 'actions_workflow_unknown',
'error-detail': 'error getting job details'
})
expect(markJobAsProcessedSpy).toHaveBeenCalled()
})
})

describe('when there is an error retrieving job credentials from DependabotAPI', () => {
beforeEach(() => {
jest
.spyOn(APIClient.prototype, 'getCredentials')
.mockImplementationOnce(
jest.fn(async () =>
Promise.reject(new Error('error getting credentials'))
)
)

process.env.GITHUB_EVENT_PATH = eventFixturePath('default')
process.env.GITHUB_EVENT_NAME = 'workflow_dispatch'
context = new Context()
})

test('it fails the workflow', async () => {
await run(context)

expect(core.setFailed).toHaveBeenCalledWith(
expect.stringContaining('error getting credentials')
)
})

test('it relays a failure message to the dependabot service', async () => {
await run(context)

expect(reportJobErrorSpy).toHaveBeenCalledWith({
'error-type': 'actions_workflow_unknown',
'error-detail': 'error getting credentials'
})
expect(markJobAsProcessedSpy).toHaveBeenCalled()
})
})

describe('when there is an error pulling images', () => {
beforeEach(() => {
jest
.spyOn(ImageService, 'pull')
.mockImplementationOnce(
jest.fn(async () =>
Promise.reject(new Error('error pulling an image'))
)
)

process.env.GITHUB_EVENT_PATH = eventFixturePath('default')
process.env.GITHUB_EVENT_NAME = 'workflow_dispatch'
context = new Context()
})

test('it fails the workflow', async () => {
await run(context)

expect(core.setFailed).toHaveBeenCalledWith(
expect.stringContaining('error pulling an image')
)
})

test('it relays a failure message to the dependabot service', async () => {
await run(context)

expect(reportJobErrorSpy).toHaveBeenCalledWith({
'error-type': 'actions_workflow_image',
'error-detail': 'error pulling an image'
})
expect(markJobAsProcessedSpy).toHaveBeenCalled()
})
})

describe('when there is an error running the update', () => {
beforeEach(() => {
jest
.spyOn(Updater.prototype, 'runUpdater')
.mockImplementationOnce(
jest.fn(async () =>
Promise.reject(new Error('error running the update'))
)
)

process.env.GITHUB_EVENT_PATH = eventFixturePath('default')
process.env.GITHUB_EVENT_NAME = 'workflow_dispatch'
context = new Context()
})

test('it fails the workflow', async () => {
await run(context)

expect(core.setFailed).toHaveBeenCalledWith(
expect.stringContaining('error running the update')
)
})

test('it relays a failure message to the dependabot service', async () => {
await run(context)

expect(reportJobErrorSpy).toHaveBeenCalledWith({
'error-type': 'actions_workflow_updater',
'error-detail': 'error running the update'
})
expect(markJobAsProcessedSpy).toHaveBeenCalled()
})
})
})
35 changes: 33 additions & 2 deletions src/api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export class JobParameters {
) {}
}

// TODO: Populate with enabled values
// TODO: Rescue unsupported values
export enum PackageManager {
NpmAndYarn = 'npm_and_yarn'
}
Expand All @@ -23,6 +25,11 @@ export type JobDetails = {
'package-manager': PackageManager
}

export type JobError = {
'error-type': string
'error-detail': any
}

export type Credential = {
type: string
host: string
Expand Down Expand Up @@ -50,8 +57,8 @@ export class APIClient {
}

async getCredentials(): Promise<Credential[]> {
const detailsURL = `/update_jobs/${this.params.jobID}/credentials`
const res = await this.client.get(detailsURL, {
const credentialsURL = `/update_jobs/${this.params.jobID}/credentials`
const res = await this.client.get(credentialsURL, {
headers: {Authorization: this.params.credentialsToken}
})
if (res.status !== 200) {
Expand All @@ -60,4 +67,28 @@ export class APIClient {

return res.data.data.attributes.credentials
}

async reportJobError(error: JobError): Promise<void> {
const recordErrorURL = `/update_jobs/${this.params.jobID}/record_update_job_error`
const res = await this.client.post(recordErrorURL, error, {
headers: {Authorization: this.params.jobToken}
})
if (res.status !== 200) {
throw new Error(`Unexpected status code: ${res.status}`)
}

return res.data.data.attributes
}

async markJobAsProcessed(): Promise<void> {
const markAsProcessedURL = `/update_jobs/${this.params.jobID}/mark_as_processed`
const res = await this.client.get(markAsProcessedURL, {
headers: {Authorization: this.params.credentialsToken}
})
if (res.status !== 200) {
throw new Error(`Unexpected status code: ${res.status}`)
}

return res.data.data.attributes
}
}
Loading

0 comments on commit 3daf83f

Please sign in to comment.