Skip to content

Commit

Permalink
Improve clean up
Browse files Browse the repository at this point in the history
  • Loading branch information
szapp committed Apr 11, 2024
1 parent 8d408c0 commit 3850867
Show file tree
Hide file tree
Showing 5 changed files with 185 additions and 25 deletions.
45 changes: 40 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,11 @@ on:
paths:
- '**.src'
- '**.d'
check_run: # This is optional, see notes below
types: completed

# These permissions are necessary for creating the check runs
permissions:
contents: read
checks: write
actions: write # This is optional, see notes below

# The checkout action needs to be run first
jobs:
Expand All @@ -58,6 +55,7 @@ jobs:
file: _work/Data/Scripts/Content/Gothic.src
check-name: # Optional (see below)
cache: # Optional
token: # Optional
```
## Configuration
Expand Down Expand Up @@ -89,8 +87,45 @@ Unfortunately, the creation of the superfluous workflow check status cannot be s

One workaround is to delete the entire workflow after the checks have been performed, effectively removing the check status from the commit.
However, this is not possible with the default `GITHUB_TOKEN`, to avoid recursive workflow runs.
To remove the additional status check, call this GitHub Action with an authentication `token` of a GitHub App and enable the `check_run` event with `completed` (see above).
To remove the additional status check, call this GitHub Action with an authentication `token` of a GitHub App and enable the `check_run` event with `completed` (see below).
For more details the issue, see [here](https://github.com/peter-murray/workflow-application-token-action#readme).
Always leave the additional input `cleanup-token` at its default.
For more details, see [here](https://github.com/peter-murray/workflow-application-token-action#readme).

Nevertheless, this is a optional cosmetic enhancement and this GitHub action works fine without.

```yaml
name: scripts
on:
push:
paths:
- '**.src'
- '**.d'
check_run:
types: completed
permissions:
contents: read
checks: write
actions: write
jobs:
parsiphae:
name: Run Parsiphae on scripts
if: github.event_name != 'check_run' || startsWith(github.event.check_run.name, 'Parsiphae') # Adjust to check name
runs-on: ubuntu-latest
steps:
- uses: actions/create-github-app-token@v1
id: app-token
with:
app-id: ${{ vars.APP_ID }} # GitHub App ID
private-key: ${{ secrets.APP_KEY }} # GitHub App private key
- uses: actions/checkout@v4
- name: Check scripts
uses: szapp/parsiphae-action@v1
with:
file: _work/Data/Scripts/Content/Gothic.src
check-name: # Optional (see below)
cache: # Optional
token: ${{ steps.app-token.outputs.token }}
```
81 changes: 74 additions & 7 deletions __tests__/cleanup.test.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,30 @@
import * as core from '@actions/core'
import * as github from '@actions/github'
import { workflow } from '../src/cleanup'
import timers from 'timers/promises'

// Mock the GitHub API
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const getWorkflowRunMock = jest.fn((_params) => ({
const getWorkflowRunMock = jest.fn(async (_params) => ({
data: { workflow_id: 123 },
}))
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const listWorkflowRunsMock = jest.fn((_params) => ({
const listWorkflowRunsMock = jest.fn(async (_params) => ({
data: {
workflow_runs: [
{ id: 1, event: 'push' },
{ id: 2, event: 'check_run' },
{ id: 3, event: 'workflow_run' },
{ id: 1, event: 'push', status: 'in_progress' },
{ id: 2, event: 'check_run', status: 'in_progress' },
{ id: 3, event: 'workflow_run', status: 'completed' },
],
},
}))
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const listWorkflowRunsForRepoMock = jest.fn(async (_params) => ({
data: {
workflow_runs: [] as { id: number; event: string }[],
},
}))
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const deleteWorkflowRunMock = jest.fn(async (_params) => {})
jest.mock('@actions/github', () => {
return {
Expand All @@ -28,6 +35,7 @@ jest.mock('@actions/github', () => {
actions: {
getWorkflowRun: getWorkflowRunMock,
listWorkflowRuns: listWorkflowRunsMock,
listWorkflowRunsForRepo: listWorkflowRunsForRepoMock,
deleteWorkflowRun: deleteWorkflowRunMock,
},
},
Expand All @@ -39,7 +47,7 @@ jest.mock('@actions/github', () => {
action: 'completed',
check_run: {
head_sha: 'abc123',
name: 'My Check Run',
name: 'Patch Validator',
html_url: 'https://example.com/check_run',
conclusion: 'success',
},
Expand All @@ -48,7 +56,7 @@ jest.mock('@actions/github', () => {
owner: 'owner',
repo: 'repo',
},
runId: 123,
runId: 2,
},
}
})
Expand All @@ -60,8 +68,12 @@ describe('cleanup', () => {
jest.spyOn(core.summary, 'addHeading').mockImplementation(() => core.summary)
jest.spyOn(core.summary, 'addRaw').mockImplementation(() => core.summary)
jest.spyOn(core.summary, 'write').mockImplementation()
jest.spyOn(core, 'info').mockImplementation()
jest.spyOn(core, 'error').mockImplementation()
jest.spyOn(core, 'setFailed').mockImplementation()
jest.spyOn(core, 'getInput').mockReturnValue('CheckName')
jest.spyOn(process, 'exit').mockImplementation()
jest.spyOn(timers, 'setTimeout').mockImplementation()
})

it('should return false if the event is not check_run or action is not completed', async () => {
Expand All @@ -71,24 +83,69 @@ describe('cleanup', () => {
const result = await workflow()

expect(result).toBe(false)
expect(listWorkflowRunsForRepoMock).not.toHaveBeenCalled()
expect(getWorkflowRunMock).not.toHaveBeenCalled()
expect(listWorkflowRunsMock).not.toHaveBeenCalled()
expect(deleteWorkflowRunMock).not.toHaveBeenCalled()
expect(core.summary.addHeading).not.toHaveBeenCalled()
expect(core.summary.addRaw).not.toHaveBeenCalled()
expect(core.summary.write).not.toHaveBeenCalled()
expect(core.error).not.toHaveBeenCalled()
expect(core.setFailed).not.toHaveBeenCalled()
expect(process.exit).not.toHaveBeenCalled()
})

it('should fail when run with the incorrect check_un', async () => {
github.context.eventName = 'check_run'
github.context.payload.action = 'completed'
github.context.payload.check_run.conclusion = 'failure'
github.context.payload.check_run.name = 'Wrong name'

const result = await workflow()

expect(result).toBe(true)
expect(timers.setTimeout).not.toHaveBeenCalled()
expect(listWorkflowRunsForRepoMock).not.toHaveBeenCalled()
expect(getWorkflowRunMock).not.toHaveBeenCalled()
expect(listWorkflowRunsMock).not.toHaveBeenCalled()
expect(deleteWorkflowRunMock).not.toHaveBeenCalled()
expect(deleteWorkflowRunMock).not.toHaveBeenCalled()
expect(core.summary.addHeading).not.toHaveBeenCalled()
expect(core.summary.addRaw).not.toHaveBeenCalled()
expect(core.summary.write).not.toHaveBeenCalled()
expect(core.error).not.toHaveBeenCalled()
expect(core.setFailed).toHaveBeenCalledWith('This action is only intended to be run on the "CheckName" check run')
})

it('should delete workflow runs and set exit code if the event is check_run and action is completed', async () => {
github.context.eventName = 'check_run'
github.context.payload.action = 'completed'
github.context.payload.check_run.conclusion = 'success'
github.context.payload.check_run.name = 'CheckName'
listWorkflowRunsForRepoMock.mockResolvedValueOnce({
data: {
workflow_runs: [{ id: 1, event: 'push' }],
},
})

const result = await workflow()

expect(result).toBe(true)
expect(timers.setTimeout).toHaveBeenCalledWith(15000)
expect(timers.setTimeout).toHaveBeenCalledTimes(2)
expect(listWorkflowRunsForRepoMock).toHaveBeenCalledWith({
...github.context.repo,
status: 'in_progress',
head_sha: github.context.payload.check_run.head_sha,
})
expect(listWorkflowRunsForRepoMock).toHaveReturnedWith(
Promise.resolve({
data: {
workflow_runs: [{ id: 1, event: 'push' }],
},
})
)
expect(listWorkflowRunsForRepoMock).toHaveBeenCalledTimes(2)
expect(getWorkflowRunMock).toHaveBeenCalledWith({
...github.context.repo,
run_id: github.context.runId,
Expand All @@ -98,6 +155,7 @@ describe('cleanup', () => {
workflow_id: 123,
head_sha: github.context.payload.check_run.head_sha,
})
expect(core.info).toHaveBeenCalledWith('Runs to delete: 1(in_progress), 3(completed)')
expect(deleteWorkflowRunMock).toHaveBeenCalledWith({
...github.context.repo,
run_id: 1,
Expand All @@ -110,18 +168,26 @@ describe('cleanup', () => {
expect(core.summary.addRaw).toHaveBeenCalledWith(`<a href="${github.context.payload.check_run.html_url}">Details</a>`, true)
expect(core.summary.write).toHaveBeenCalledWith({ overwrite: false })
expect(core.error).not.toHaveBeenCalled()
expect(core.setFailed).not.toHaveBeenCalled()
expect(process.exitCode).toBe(core.ExitCode.Success)
})

it('should handle errors when deleting workflow runs', async () => {
github.context.eventName = 'check_run'
github.context.payload.action = 'completed'
github.context.payload.check_run.conclusion = 'failure'
github.context.payload.check_run.name = 'CheckName: some file'
deleteWorkflowRunMock.mockRejectedValueOnce(new Error('Delete error'))

const result = await workflow()

expect(result).toBe(true)
expect(timers.setTimeout).toHaveBeenCalledWith(15000)
expect(listWorkflowRunsForRepoMock).toHaveBeenCalledWith({
...github.context.repo,
status: 'in_progress',
head_sha: github.context.payload.check_run.head_sha,
})
expect(getWorkflowRunMock).toHaveBeenCalledWith({
...github.context.repo,
run_id: github.context.runId,
Expand All @@ -143,6 +209,7 @@ describe('cleanup', () => {
expect(core.summary.addRaw).toHaveBeenCalledWith(`<a href="${github.context.payload.check_run.html_url}">Details</a>`, true)
expect(core.summary.write).toHaveBeenCalledWith({ overwrite: false })
expect(core.error).toHaveBeenCalledWith(new Error('Delete error'))
expect(core.setFailed).not.toHaveBeenCalled()
expect(process.exitCode).toBe(core.ExitCode.Failure)
})
})
39 changes: 35 additions & 4 deletions 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.

43 changes: 35 additions & 8 deletions src/cleanup.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,34 @@
import * as core from '@actions/core'
import * as github from '@actions/github'
import { setTimeout } from 'timers/promises'

export async function workflow(): Promise<boolean> {
// Only for completed check runs
if (github.context.eventName !== 'check_run' || github.context.payload.action !== 'completed') return false
const octokit = github.getOctokit(core.getInput('cleanup-token'))
const checkName = core.getInput('check-name')

// Check if the triggering check run is the correct one
if (!github.context.payload.check_run.name.startsWith(checkName)) {
// This workflow run here will then be also deleted by the correctly triggered run
core.setFailed(`This action is only intended to be run on the "${checkName}" check run`)
return true
}

// Let all running workflows finish
let status: boolean
do {
core.info('Waiting for any workflow runs to finish...')
await setTimeout(15000) // Give some time for all workflows to start up
const {
data: { workflow_runs },
} = await octokit.rest.actions.listWorkflowRunsForRepo({
...github.context.repo,
status: 'in_progress',
head_sha: github.context.payload.check_run.head_sha,
})
status = workflow_runs.some((w) => w.event !== 'check_run')
} while (status)

// First, get the workflow ID
const {
Expand All @@ -23,16 +47,19 @@ export async function workflow(): Promise<boolean> {
head_sha: github.context.payload.check_run.head_sha,
})

// For all workflow runs that are not check runs, delete them
const workflows = workflow_runs.filter((w) => w.event !== 'check_run')
Promise.all(
// Delete all workflow runs except the current one
const workflows = workflow_runs.filter((w) => w.id !== github.context.runId)
core.info(`Runs to delete: ${workflows.map((w) => `${w.id}(${w.status})`).join(', ')}`)
Promise.allSettled(
workflows.map((w) =>
octokit.rest.actions.deleteWorkflowRun({
...github.context.repo,
run_id: w.id,
})
octokit.rest.actions
.deleteWorkflowRun({
...github.context.repo,
run_id: w.id,
})
.catch((error) => core.error(error))
)
).catch((error) => core.error(error))
)

// The summary of the workflow runs is unfortunately not available in the API
// So we can only link to the check run
Expand Down

0 comments on commit 3850867

Please sign in to comment.