Skip to content

Commit

Permalink
Run action post command on teardown to set reactions
Browse files Browse the repository at this point in the history
In case the job was cancelled, or failed for other reasons,
we still want to try and update the comment reaction to failed
if possible.

Change-type: patch
Signed-off-by: Kyle Harding <[email protected]>
  • Loading branch information
klutchell committed Oct 17, 2024
1 parent 8aa7214 commit a0d5fe5
Show file tree
Hide file tree
Showing 9 changed files with 256 additions and 3 deletions.
7 changes: 7 additions & 0 deletions __tests__/approval.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ describe('ApprovalProcess', () => {
expect(mockGitHubClient.createIssueComment).toHaveBeenCalledWith(
commentBody
)
expect(core.saveState).toHaveBeenCalledWith(
'comment-id',
'test-comment-id'
)
expect(core.setOutput).toHaveBeenCalledWith(
'comment-id',
'test-comment-id'
Expand Down Expand Up @@ -126,6 +130,7 @@ describe('ApprovalProcess', () => {
)
await waitPromise

expect(core.saveState).toHaveBeenCalledWith('approved-by', 'approver')
expect(core.setOutput).toHaveBeenCalledWith('approved-by', 'approver')
expect(core.info).toHaveBeenCalledWith('Workflow approved by approver')
})
Expand All @@ -143,6 +148,7 @@ describe('ApprovalProcess', () => {

await expect(waitPromise).rejects.toThrow('Workflow rejected by rejector')
expect(mockReactionManager.getEligibleReactions).toHaveBeenCalledTimes(2)
expect(core.saveState).toHaveBeenCalledWith('rejected-by', 'rejector')
expect(core.setOutput).toHaveBeenCalledWith('rejected-by', 'rejector')
}, 2000) // Set test timeout to 2 seconds

Expand All @@ -162,6 +168,7 @@ describe('ApprovalProcess', () => {
await waitPromise

expect(mockReactionManager.getEligibleReactions).toHaveBeenCalledTimes(3)
expect(core.saveState).toHaveBeenCalledWith('approved-by', 'approver')
expect(core.setOutput).toHaveBeenCalledWith('approved-by', 'approver')
}, 2000) // Set test timeout to 2 seconds
})
Expand Down
56 changes: 56 additions & 0 deletions __tests__/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ const github = require('@actions/github')
const { GitHubClient } = require('../src/client')
const { ReactionManager } = require('../src/reactions')
const { ApprovalProcess } = require('../src/approval')
const { PostProcess } = require('../src/post')

jest.mock('@actions/core')
jest.mock('@actions/github')
jest.mock('../src/client')
jest.mock('../src/reactions')
jest.mock('../src/approval')
jest.mock('../src/post')

describe('index.js', () => {
let run
Expand Down Expand Up @@ -122,4 +124,58 @@ describe('index.js', () => {
})
)
})

test('run executes the post process successfully', async () => {
core.getInput.mockImplementation(name => {
const inputs = {
'github-token': 'mock-token'
}
return inputs[name]
})

core.getState.mockImplementation(key => {
const states = {
'comment-id': 'test-comment-id',
'approved-by': 'test-approver',
isPost: 'true'
}
return states[key]
})

// Mock GitHub client
const mockOctokit = {}
github.getOctokit.mockReturnValue(mockOctokit)

// Mock PostProcess run method
// PostProcess.mockImplementation(() => ({
// run: jest.fn().mockResolvedValue(undefined)
// }))
// FIXME: Why does the above not mock the run method?
jest.mock('../src/post', () => ({
PostProcess: jest.fn().mockImplementation(() => ({
run: jest.fn().mockResolvedValue(undefined)
}))
}))

await run()

// Verify that the GitHub client was created
expect(github.getOctokit).toHaveBeenCalledWith('mock-token')
expect(GitHubClient).toHaveBeenCalledWith(mockOctokit, github.context)

// Verify that ReactionManager was created
expect(ReactionManager).toHaveBeenCalled()

// Verify that PostProcess was created
expect(PostProcess).toHaveBeenCalledWith(
expect.any(GitHubClient),
expect.any(ReactionManager)
)

// Verify that the post process was run
expect(PostProcess.mock.instances[0].run).toHaveBeenCalled()

// Verify that no error was set
expect(core.setFailed).not.toHaveBeenCalled()
})
})
85 changes: 85 additions & 0 deletions __tests__/post.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
const core = require('@actions/core')
const { PostProcess } = require('../src/post')

jest.mock('@actions/core')

describe('PostProcess', () => {
let postProcess
let mockGitHubClient
let mockReactionManager

beforeEach(() => {
jest.clearAllMocks()

mockGitHubClient = {
getAuthenticatedUser: jest.fn()
}

mockReactionManager = {
setReaction: jest.fn(),
reactions: {
SUCCESS: 'rocket',
FAILED: 'confused'
}
}

postProcess = new PostProcess(mockGitHubClient, mockReactionManager)
})

describe('run', () => {
beforeEach(() => {
core.getState.mockImplementation(key => {
const states = {
'comment-id': 'test-comment-id',
'approved-by': 'test-approver'
}
return states[key]
})
mockGitHubClient.getAuthenticatedUser.mockResolvedValue({
id: 'test-user-id'
})
})

test('creates success reaction when approval is successful', async () => {
await postProcess.run()

expect(mockGitHubClient.getAuthenticatedUser).toHaveBeenCalled()
expect(mockReactionManager.setReaction).toHaveBeenCalledWith(
'test-comment-id',
'test-user-id',
mockReactionManager.reactions.SUCCESS
)
})

test('creates failed reaction when approval is not successful', async () => {
core.getState.mockImplementation(key => {
const states = {
'comment-id': 'test-comment-id',
'approved-by': ''
}
return states[key]
})

await postProcess.run()

expect(mockGitHubClient.getAuthenticatedUser).toHaveBeenCalled()
expect(mockReactionManager.setReaction).toHaveBeenCalledWith(
'test-comment-id',
'test-user-id',
mockReactionManager.reactions.FAILED
)
})

test('logs a warning when an error occurs', async () => {
mockGitHubClient.getAuthenticatedUser.mockRejectedValue(
new Error('Authentication failed')
)

await postProcess.run()

expect(core.warning).toHaveBeenCalledWith(
'Cleanup failed: Authentication failed'
)
})
})
})
2 changes: 2 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,5 @@ outputs:
runs:
using: 'node20'
main: dist/index.js
post: dist/index.js
post-if: 'always()'
57 changes: 56 additions & 1 deletion dist/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions src/approval.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class ApprovalProcess {

const comment = await this.gitHubClient.createIssueComment(commentBody)

core.saveState('comment-id', comment.id)
core.setOutput('comment-id', comment.id)

await this.reactionManager.setReaction(
Expand Down Expand Up @@ -73,6 +74,7 @@ class ApprovalProcess {

if (rejectedBy) {
core.debug(`Workflow rejected by ${rejectedBy}`)
core.saveState('rejected-by', rejectedBy)
core.setOutput('rejected-by', rejectedBy)
throw new Error(`Workflow rejected by ${rejectedBy}`)
}
Expand All @@ -82,6 +84,7 @@ class ApprovalProcess {
)?.user.login

if (approvedBy) {
core.saveState('approved-by', approvedBy)
core.setOutput('approved-by', approvedBy)
core.info(`Workflow approved by ${approvedBy}`)
return
Expand Down
12 changes: 11 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const github = require('@actions/github')
const { GitHubClient } = require('./client')
const { ReactionManager } = require('./reactions')
const { ApprovalProcess } = require('./approval')
const { PostProcess } = require('./post')

async function run() {
try {
Expand All @@ -26,12 +27,21 @@ async function run() {
const octokit = github.getOctokit(config.token)
const gitHubClient = new GitHubClient(octokit, github.context)
const reactionManager = new ReactionManager(gitHubClient)

// Check if this is a post-execution run
// eslint-disable-next-line no-extra-boolean-cast
if (!!core.getState('isPost')) {
const postProcess = new PostProcess(gitHubClient, reactionManager)
await postProcess.run()
return
}

core.saveState('isPost', 'true')
const approvalProcess = new ApprovalProcess(
gitHubClient,
reactionManager,
config
)

await approvalProcess.run()
} catch (error) {
core.setFailed(error.message)
Expand Down
35 changes: 35 additions & 0 deletions src/post.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
const core = require('@actions/core')

class PostProcess {
constructor(gitHubClient, reactionManager) {
this.gitHubClient = gitHubClient
this.reactionManager = reactionManager
}

async run() {
try {
const commentId = core.getState('comment-id')
const wasApproved = core.getState('approved-by') !== ''
const tokenUser = await this.gitHubClient.getAuthenticatedUser()

if (commentId && wasApproved) {
await this.reactionManager.setReaction(
commentId,
tokenUser.id,
this.reactionManager.reactions.SUCCESS
)
return
}
await this.reactionManager.setReaction(
commentId,
tokenUser.id,
this.reactionManager.reactions.FAILED
)
} catch (error) {
core.warning(`Cleanup failed: ${error.message}`)
console.trace()
}
}
}

module.exports = { PostProcess }

0 comments on commit a0d5fe5

Please sign in to comment.