blob: 109dd86263ac65ca9fefccf1d7fccce371809e0f [file] [log] [blame]
import * as github from '@actions/github'
import * as core from '@actions/core'
import * as rest from '@octokit/rest'
/**
Those are the cancellable event types tht we know about
*/
const CANCELLABLE_EVENT_TYPES = [
'push',
'pull_request',
'workflow_run',
'schedule',
'workflow_dispatch'
]
/**
* Those are different modes for cancelling
*/
enum CancelMode {
DUPLICATES = 'duplicates',
ALL_DUPLICATES = 'allDuplicates',
SELF = 'self',
FAILED_JOBS = 'failedJobs',
NAMED_JOBS = 'namedJobs',
ALL_DUPLICATED_NAMED_JOBS = 'allDuplicatedNamedJobs'
}
/**
* Stores information about the owner and repository used, as well as octokit object that is used for
* authentication.
*/
interface RepositoryInfo {
octokit: github.GitHub
owner: string
repo: string
}
/**
* Stores information about the workflow info that triggered the current workflow.
*/
interface TriggeringRunInfo {
workflowId: number
runId: number
headRepo: string
headBranch: string
headSha: string
eventName: string
mergeCommitSha: string | null
pullRequest: rest.PullsListResponseItem | null
}
/**
* Converts the source of a run object into a string that can be used as map key in maps where we keep
* arrays of runs per source group
* @param triggeringRunInfo the object identifying the triggering workflow
* @param sourceWorkflowId - workflow id to act on
* @returns the unique string id for the group
*/
function getCommonGroupIdFromTriggeringRunInfo(
triggeringRunInfo: TriggeringRunInfo,
sourceWorkflowId: string | number
): string {
return (
`:${sourceWorkflowId}:${triggeringRunInfo.headRepo}` +
`:${triggeringRunInfo.headBranch}:${triggeringRunInfo.eventName}`
)
}
/**
* Converts the source of a run object into a string that can be used as map key in maps where we keep
* arrays of runs per group
* @param runItem Item the run item to retrieve the group from
* @returns the unique string id for the group
*/
function getCommonGroupIdFromRunItem(
runItem: rest.ActionsListWorkflowRunsResponseWorkflowRunsItem
): string {
return `:${retrieveWorkflowIdFromUrl(runItem.workflow_url)}:${
runItem.head_repository.full_name
}:${runItem.head_branch}:${runItem.event}`
}
/**
* Creates query parameters selecting all runs that share the same source group as we have. This can
* be used to select duplicates of my own run.
*
* @param repositoryInfo - information about the repository used
* @param status - status of the run that we are querying for
* @param triggeringRunInfo - information about the workflow that triggered the run
* @param sourceWorkflowId - workflow id to act on
* @return query parameters merged with the listWorkflowRuns criteria
*/
function createListRunsQueryRunsSameSource(
repositoryInfo: RepositoryInfo,
sourceWorkflowId: string | number,
status: string,
triggeringRunInfo: TriggeringRunInfo
): rest.RequestOptions {
const request = {
owner: repositoryInfo.owner,
repo: repositoryInfo.repo,
// eslint-disable-next-line @typescript-eslint/camelcase
workflow_id: sourceWorkflowId,
status,
branch: triggeringRunInfo.headBranch,
event: triggeringRunInfo.eventName
}
return repositoryInfo.octokit.actions.listWorkflowRuns.endpoint.merge(request)
}
/**
* Creates query parameters selecting only specific run Id.
* @param repositoryInfo - information about the repository used
* @param sourceWorkflowId - workflow id to act on
* @param status - status of the run that we are querying for
* @param triggeringRunInfo - information about the workflow that triggered the run
* @return query parameters merged with the listWorkflowRuns criteria
*/
function createListRunsQuerySpecificRunId(
repositoryInfo: RepositoryInfo,
sourceWorkflowId: string | number,
status: string,
triggeringRunInfo: TriggeringRunInfo
): rest.RequestOptions {
const request = {
owner: repositoryInfo.owner,
repo: repositoryInfo.repo,
// eslint-disable-next-line @typescript-eslint/camelcase
workflow_id: sourceWorkflowId,
status,
// eslint-disable-next-line @typescript-eslint/camelcase
run_id: triggeringRunInfo.runId.toString()
}
return repositoryInfo.octokit.actions.listWorkflowRuns.endpoint.merge(request)
}
/**
* Creates query parameters selecting all run Ids for specified workflow Id.
* @param repositoryInfo - information about the repository used
* @param status - status of the run that we are querying for
* @param workflowId - Id of the workflow to retrieve
* @return query parameters merged with the listWorkflowRuns criteria
*/
function createListRunsQueryAllRuns(
repositoryInfo: RepositoryInfo,
status: string,
workflowId: number | string
): rest.RequestOptions {
const request = {
owner: repositoryInfo.owner,
repo: repositoryInfo.repo,
// eslint-disable-next-line @typescript-eslint/camelcase
workflow_id: workflowId,
status
}
return repositoryInfo.octokit.actions.listWorkflowRuns.endpoint.merge(request)
}
/**
* Creates query parameters selecting all jobs for specified run Id.
* @param repositoryInfo - information about the repository used
* @param runId - Id of the run to retrieve jobs for
* @return query parameters merged with the listJobsForWorkflowRun criteria
*/
function createJobsForWorkflowRunQuery(
repositoryInfo: RepositoryInfo,
runId: number
): rest.RequestOptions {
const request = {
owner: repositoryInfo.owner,
repo: repositoryInfo.repo,
// eslint-disable-next-line @typescript-eslint/camelcase
run_id: runId
}
return repositoryInfo.octokit.actions.listJobsForWorkflowRun.endpoint.merge(
request
)
}
/**
* Returns true if the string matches any of the regexps in array of regexps
* @param stringToMatch string to match
* @param regexps array of regexp to match the string against
* @return array of [matched (boolean), [array of matched strings]]
*/
function matchInArray(
stringToMatch: string,
regexps: string[]
): [boolean, string[]] {
let matched = false
const allMatches: string[] = []
for (const regexp of regexps) {
const match = stringToMatch.match(regexp)
if (match) {
matched = true
allMatches.push(match[0])
}
}
return [matched, allMatches]
}
/**
* Adds workflow run to the array in the map indexed by key.
* @param key key to use
* @param runItem run Item to add
* @param mapOfWorkflowRunCandidates map of workflow runs to add the run item to
*/
function addWorkflowRunToMap(
key: string,
runItem: rest.ActionsListWorkflowRunsResponseWorkflowRunsItem,
mapOfWorkflowRunCandidates: Map<
string,
rest.ActionsListWorkflowRunsResponseWorkflowRunsItem[]
>
): void {
core.info(`\nAdding the run: ${runItem.id} to candidates with ${key} key.\n`)
let arrayOfRuns = mapOfWorkflowRunCandidates.get(key)
if (arrayOfRuns === undefined) {
arrayOfRuns = []
mapOfWorkflowRunCandidates.set(key, arrayOfRuns)
}
arrayOfRuns.push(runItem)
}
/**
* Returns true if the runId specified has jobs matching the regexp and optionally checks if those
* jobs are failed.
* * If checkIfFailed is False, it returns true if any of the job name for the run match any of the regexps
* * Id checkIfFailed is True, it returns true if any of the matching jobs have status 'failed'
* @param repositoryInfo - information about the repository used
* @param runId - Id of the run to retrieve jobs for
* @param jobNameRegexps - array of job name regexps
* @param checkIfFailed - whether to check the 'failed' status of matched jobs
* @return array of [matched (boolean), array of matches]
*/
async function jobsMatchingNames(
repositoryInfo: RepositoryInfo,
runId: number,
jobNameRegexps: string[],
checkIfFailed: boolean
): Promise<[boolean, string[]]> {
const listJobs = createJobsForWorkflowRunQuery(repositoryInfo, runId)
if (checkIfFailed) {
core.info(
`\nChecking if runId ${runId} has job names matching any of the ${jobNameRegexps} that failed\n`
)
} else {
core.info(
`\nChecking if runId ${runId} has job names matching any of the ${jobNameRegexps}\n`
)
}
const allMatches: string[] = []
let matched = false
for await (const item of repositoryInfo.octokit.paginate.iterator(listJobs)) {
for (const job of item.data.jobs) {
core.info(` The job name: ${job.name}, Conclusion: ${job.conclusion}`)
const [jobMatched, jobMatches] = matchInArray(job.name, jobNameRegexps)
if (jobMatched) {
allMatches.push(...jobMatches)
if (checkIfFailed) {
// Only fail the build if one of the matching jobs fail
if (job.conclusion === 'failure') {
core.info(
` The Job ${job.name} matches one of the ${jobNameRegexps} regexps and it failed.` +
` It will be added to the candidates.`
)
matched = true
} else {
core.info(
` The Job ${job.name} matches one of the ${jobNameRegexps} regexps but it did not fail. ` +
` So far, so good.`
)
}
} else {
// Fail the build if any of the job names match
core.info(
` The Job ${job.name} matches one of the ${jobNameRegexps} regexps. ` +
`It will be added to the candidates.`
)
matched = true
}
}
}
}
return [matched, allMatches]
}
/**
* Retrieves workflowId from the workflow URL.
* @param workflowUrl workflow URL to retrieve the ID from
* @return numerical workflow id
*/
function retrieveWorkflowIdFromUrl(workflowUrl: string): number {
const workflowIdString = workflowUrl.split('/').pop() || ''
if (!(workflowIdString.length > 0)) {
throw new Error('Could not resolve workflow')
}
return parseInt(workflowIdString)
}
/**
* Returns workflowId of the runId specified
* @param repositoryInfo - information about the repository used
* @param runId - Id of the run to retrieve jobs for
* @return workflow ID for the run id
*/
async function getWorkflowId(
repositoryInfo: RepositoryInfo,
runId: number
): Promise<number> {
const reply = await repositoryInfo.octokit.actions.getWorkflowRun({
owner: repositoryInfo.owner,
repo: repositoryInfo.repo,
// eslint-disable-next-line @typescript-eslint/camelcase
run_id: runId
})
core.info(`The source run ${runId} is in ${reply.data.workflow_url} workflow`)
return retrieveWorkflowIdFromUrl(reply.data.workflow_url)
}
/**
* Returns workflow runs matching the callable adding query criteria
* @param repositoryInfo - information about the repository used
* @param statusValues - array of string status values for runs that we are interested at
* @param cancelMode - which cancel mode the query is about
* @param createListRunQuery - what is the callable criteria selection
* @return map of workflow run items indexed by the workflow run number
*/
async function getWorkflowRuns(
repositoryInfo: RepositoryInfo,
statusValues: string[],
cancelMode: CancelMode,
createListRunQuery: CallableFunction
): Promise<Map<number, rest.ActionsListWorkflowRunsResponseWorkflowRunsItem>> {
const workflowRuns = new Map<
number,
rest.ActionsListWorkflowRunsResponseWorkflowRunsItem
>()
for (const status of statusValues) {
const listRuns = await createListRunQuery(status)
for await (const item of repositoryInfo.octokit.paginate.iterator(
listRuns
)) {
// There is some sort of bug where the pagination URLs point to a
// different endpoint URL which trips up the resulting representation
// In that case, fallback to the actual REST 'workflow_runs' property
const elements =
item.data.length === undefined ? item.data.workflow_runs : item.data
for (const element of elements) {
workflowRuns.set(element.run_number, element)
}
}
}
core.info(`\nFound runs: ${Array.from(workflowRuns).map(t => t[0])}\n`)
return workflowRuns
}
/**
* True if the request is candidate for cancelling in case of duplicate deletion
* @param runItem item to check
* @param cancelFutureDuplicates whether future duplicates are being cancelled
* @param triggeringRunInfo - information about the workflow that triggered the run
* @param sourceWorkflowId - workflow id to act on
* @param mapOfWorkflowRunCandidates - map of the workflow runs to add candidates to
* @return true if we determine that the run Id should be cancelled
*/
function checkCandidateForCancellingDuplicate(
runItem: rest.ActionsListWorkflowRunsResponseWorkflowRunsItem,
cancelFutureDuplicates: boolean,
triggeringRunInfo: TriggeringRunInfo,
sourceWorkflowId: string | number,
mapOfWorkflowRunCandidates: Map<
string,
rest.ActionsListWorkflowRunsResponseWorkflowRunsItem[]
>
): void {
const runHeadRepo = runItem.head_repository.full_name
if (
triggeringRunInfo.headRepo !== undefined &&
runHeadRepo !== triggeringRunInfo.headRepo
) {
core.info(
`\nThe run ${runItem.id} is from a different ` +
`repo: ${runHeadRepo} (expected ${triggeringRunInfo.headRepo}). Not adding as candidate to cancel.\n`
)
}
if (cancelFutureDuplicates) {
core.info(
`\nCancel Future Duplicates: Returning run id that might be duplicate or my own run: ${runItem.id}.\n`
)
addWorkflowRunToMap(
getCommonGroupIdFromTriggeringRunInfo(
triggeringRunInfo,
sourceWorkflowId
),
runItem,
mapOfWorkflowRunCandidates
)
} else {
if (runItem.id === triggeringRunInfo.runId) {
core.info(`\nThis is my own run ${runItem.id}. Not returning myself!\n`)
} else if (runItem.id > triggeringRunInfo.runId) {
core.info(
`\nThe run ${runItem.id} is started later than my own ` +
`run ${triggeringRunInfo.runId}. Not returning it\n`
)
}
core.info(`\nFound duplicate of my own run: ${runItem.id}.\n`)
}
}
/**
* Should the run be candidate for cancelling in SELF cancelling mode?
* @param runItem run item
* @param triggeringRunInfo - information about the workflow that triggered the run
* @param mapOfWorkflowRunCandidates - map of the workflow runs to add candidates to
*/
function checkCandidateForCancellingSelf(
runItem: rest.ActionsListWorkflowRunsResponseWorkflowRunsItem,
triggeringRunInfo: TriggeringRunInfo,
mapOfWorkflowRunCandidates: Map<
string,
rest.ActionsListWorkflowRunsResponseWorkflowRunsItem[]
>
): void {
if (runItem.id === triggeringRunInfo.runId) {
addWorkflowRunToMap('selfRun', runItem, mapOfWorkflowRunCandidates)
}
}
/**
* Should the run be candidate for cancelling of all duplicates
* @param runItem run item
* @param triggeringRunInfo - information about the workflow that triggered the run
* @param mapOfWorkflowRunCandidates - map of the workflow runs to add candidates to
*/
function checkCandidateForAllDuplicates(
runItem: rest.ActionsListWorkflowRunsResponseWorkflowRunsItem,
triggeringRunInfo: TriggeringRunInfo,
mapOfWorkflowRunCandidates: Map<
string,
rest.ActionsListWorkflowRunsResponseWorkflowRunsItem[]
>
): void {
addWorkflowRunToMap(
getCommonGroupIdFromRunItem(runItem),
runItem,
mapOfWorkflowRunCandidates
)
}
/**
* Should the run is candidate for cancelling in naming job cancelling mode?
* @param repositoryInfo - information about the repository used
* @param runItem run item
* @param jobNamesRegexps array of regexps to match job names against
* @param triggeringRunInfo - information about the workflow that triggered the run
* @param mapOfWorkflowRunCandidates - map of the workflow runs to add candidates to
* @return true if the run item contains jobs with names matching the pattern
*/
async function checkCandidateForCancellingNamedJobs(
repositoryInfo: RepositoryInfo,
runItem: rest.ActionsListWorkflowRunsResponseWorkflowRunsItem,
jobNamesRegexps: string[],
triggeringRunInfo: TriggeringRunInfo,
mapOfWorkflowRunCandidates: Map<
string,
rest.ActionsListWorkflowRunsResponseWorkflowRunsItem[]
>
): Promise<void> {
// Cancel all jobs that have failed jobs (no matter when started)
const [matched, allMatches] = await jobsMatchingNames(
repositoryInfo,
runItem.id,
jobNamesRegexps,
false
)
if (matched) {
core.info(
`\nSome jobs have matching names in ${runItem.id}: ${allMatches}. Adding it as candidate.\n`
)
addWorkflowRunToMap(
'allMatchingNamedJobs',
runItem,
mapOfWorkflowRunCandidates
)
} else {
core.info(`\nNone of the jobs match name in ${runItem.id}.\n`)
}
}
/**
* Should the run is candidate for cancelling in failed job cancelling mode?
* @param repositoryInfo - information about the repository used
* @param runItem run item
* @param jobNamesRegexps array of regexps to match job names against
* @param triggeringRunInfo - information about the workflow that triggered the run
* @param mapOfWorkflowRunCandidates - map of the workflow runs to add candidates to
* @return true if the run item contains failed jobs with names matching the pattern
*/
async function checkCandidateForCancellingFailedJobs(
repositoryInfo: RepositoryInfo,
runItem: rest.ActionsListWorkflowRunsResponseWorkflowRunsItem,
jobNamesRegexps: string[],
triggeringRunInfo: TriggeringRunInfo,
mapOfWorkflowRunCandidates: Map<
string,
rest.ActionsListWorkflowRunsResponseWorkflowRunsItem[]
>
): Promise<void> {
// Cancel all jobs that have failed jobs (no matter when started)
const [matched, allMatches] = await jobsMatchingNames(
repositoryInfo,
runItem.id,
jobNamesRegexps,
true
)
if (matched) {
core.info(
`\nSome matching named jobs failed in ${runItem.id}: ${allMatches}. Adding it to candidates.\n`
)
addWorkflowRunToMap('failedJobs', runItem, mapOfWorkflowRunCandidates)
} else {
core.info(
`\nNone of the matching jobs failed in ${runItem.id}. Not adding as candidate to cancel.\n`
)
}
}
/**
* Checks if the run is candidate for duplicate cancelling of named jobs. It adds it to the map
* including the match as a key for duplicate detection.
* @param repositoryInfo - information about the repository used
* @param runItem - run item
* @param jobNamesRegexps - array of regexps to match job names against
* @param mapOfWorkflowRunCandidates - map of runs to update
*/
async function checkCandidateForDuplicateNamedJobs(
repositoryInfo: RepositoryInfo,
runItem: rest.ActionsListWorkflowRunsResponseWorkflowRunsItem,
jobNamesRegexps: string[],
mapOfWorkflowRunCandidates: Map<
string,
rest.ActionsListWorkflowRunsResponseWorkflowRunsItem[]
>
): Promise<void> {
const [matched, allMatches] = await jobsMatchingNames(
repositoryInfo,
runItem.id,
jobNamesRegexps,
false
)
if (matched) {
for (const match of allMatches) {
// This is the only case where we are not using source group to cancelling candidates but
// the match of job names
addWorkflowRunToMap(match, runItem, mapOfWorkflowRunCandidates)
}
}
}
/**
* Determines whether the run is candidate to be cancelled depending on the mode used and add it to the map
* of workflow names if it is.
* @param repositoryInfo - information about the repository used
* @param sourceWorkflowId - workflow id to act on
* @param runItem - run item
* @param triggeringRunInfo - information about the workflow that triggered the run
* @param cancelMode - cancel mode
* @param cancelFutureDuplicates - whether to cancel future duplicates
* @param jobNamesRegexps - what are the regexps for job names
* @param skipEventTypes - which events should be skipped
* @param mapOfWorkflowRunCandidates - map of workflow runs to add candidates to
*/
async function checkCandidateForCancelling(
repositoryInfo: RepositoryInfo,
sourceWorkflowId: string | number,
runItem: rest.ActionsListWorkflowRunsResponseWorkflowRunsItem,
triggeringRunInfo: TriggeringRunInfo,
cancelMode: CancelMode,
cancelFutureDuplicates: boolean,
jobNamesRegexps: string[],
skipEventTypes: string[],
mapOfWorkflowRunCandidates: Map<
string,
rest.ActionsListWorkflowRunsResponseWorkflowRunsItem[]
>
): Promise<void> {
if ('completed' === runItem.status.toString()) {
core.info(
`\nThe run ${runItem.id} is completed. Not adding as candidate to cancel.\n`
)
return
}
if (!CANCELLABLE_EVENT_TYPES.includes(runItem.event.toString())) {
core.info(
`\nThe run ${runItem.id} is (${runItem.event} event - not ` +
`in ${CANCELLABLE_EVENT_TYPES}). Not adding as candidate to cancel.\n`
)
return
}
if (skipEventTypes.includes(runItem.event.toString())) {
core.info(
`\nThe run ${runItem.id} is (${runItem.event} event - ` +
`it is in skipEventTypes ${skipEventTypes}). Not adding as candidate to cancel.\n`
)
return
}
if (cancelMode === CancelMode.FAILED_JOBS) {
await checkCandidateForCancellingFailedJobs(
repositoryInfo,
runItem,
jobNamesRegexps,
triggeringRunInfo,
mapOfWorkflowRunCandidates
)
} else if (cancelMode === CancelMode.NAMED_JOBS) {
await checkCandidateForCancellingNamedJobs(
repositoryInfo,
runItem,
jobNamesRegexps,
triggeringRunInfo,
mapOfWorkflowRunCandidates
)
} else if (cancelMode === CancelMode.SELF) {
checkCandidateForCancellingSelf(
runItem,
triggeringRunInfo,
mapOfWorkflowRunCandidates
)
} else if (cancelMode === CancelMode.DUPLICATES) {
checkCandidateForCancellingDuplicate(
runItem,
cancelFutureDuplicates,
triggeringRunInfo,
sourceWorkflowId,
mapOfWorkflowRunCandidates
)
} else if (cancelMode === CancelMode.ALL_DUPLICATES) {
checkCandidateForAllDuplicates(
runItem,
triggeringRunInfo,
mapOfWorkflowRunCandidates
)
} else if (cancelMode === CancelMode.ALL_DUPLICATED_NAMED_JOBS) {
await checkCandidateForDuplicateNamedJobs(
repositoryInfo,
runItem,
jobNamesRegexps,
mapOfWorkflowRunCandidates
)
return
} else {
throw Error(`\nWrong cancel mode ${cancelMode}! Please correct it!.\n`)
}
}
/**
* Cancels the specified workflow run.
* @param repositoryInfo - information about the repository used
* @param runId - run Id to cancel
*/
async function cancelRun(
repositoryInfo: RepositoryInfo,
runId: number
): Promise<void> {
let reply
try {
reply = await repositoryInfo.octokit.actions.cancelWorkflowRun({
owner: repositoryInfo.owner,
repo: repositoryInfo.repo,
// eslint-disable-next-line @typescript-eslint/camelcase
run_id: runId
})
core.info(`\nThe run ${runId} cancelled, status = ${reply.status}\n`)
} catch (error) {
core.warning(
`\nCould not cancel run ${runId}: [${error.status}] ${error.message}\n`
)
}
}
/**
* Returns map of workflow run items matching the criteria specified group by workflow run id
* @param repositoryInfo - information about the repository used
* @param sourceWorkflowId - workflow id to act on
* @param statusValues - status values we want to check
* @param cancelMode - cancel mode to use
* @param triggeringRunInfo - information about the workflow that triggered the run
* @return map of the run items matching grouped by workflow run id
*/
async function getWorkflowRunsMatchingCriteria(
repositoryInfo: RepositoryInfo,
sourceWorkflowId: string | number,
statusValues: string[],
cancelMode: CancelMode,
triggeringRunInfo: TriggeringRunInfo
): Promise<Map<number, rest.ActionsListWorkflowRunsResponseWorkflowRunsItem>> {
return await getWorkflowRuns(
repositoryInfo,
statusValues,
cancelMode,
function(status: string) {
if (cancelMode === CancelMode.SELF) {
core.info(
`\nFinding runs for my own run: Owner: ${repositoryInfo.owner}, Repo: ${repositoryInfo.repo}, ` +
`Workflow ID:${sourceWorkflowId},` +
`Source Run id: ${triggeringRunInfo.runId}\n`
)
return createListRunsQuerySpecificRunId(
repositoryInfo,
sourceWorkflowId,
status,
triggeringRunInfo
)
} else if (
cancelMode === CancelMode.FAILED_JOBS ||
cancelMode === CancelMode.NAMED_JOBS ||
cancelMode === CancelMode.ALL_DUPLICATES ||
cancelMode === CancelMode.ALL_DUPLICATED_NAMED_JOBS
) {
core.info(
`\nFinding runs for all runs: Owner: ${repositoryInfo.owner}, Repo: ${repositoryInfo.repo}, ` +
`Status: ${status} Workflow ID:${sourceWorkflowId}\n`
)
return createListRunsQueryAllRuns(
repositoryInfo,
status,
sourceWorkflowId
)
} else if (cancelMode === CancelMode.DUPLICATES) {
core.info(
`\nFinding duplicate runs: Owner: ${repositoryInfo.owner}, Repo: ${repositoryInfo.repo}, ` +
`Status: ${status} Workflow ID:${sourceWorkflowId}, ` +
`Head Branch: ${triggeringRunInfo.headBranch},` +
`Event name: ${triggeringRunInfo.eventName}\n`
)
return createListRunsQueryRunsSameSource(
repositoryInfo,
sourceWorkflowId,
status,
triggeringRunInfo
)
} else {
throw Error(`\nWrong cancel mode ${cancelMode}! Please correct it.\n`)
}
}
)
}
/**
* Finds pull request matching its headRepo, headBranch and headSha
* @param repositoryInfo - information about the repository used
* @param headRepo - head repository from which Pull Request comes
* @param headBranch - head branch from which Pull Request comes
* @param headSha - sha for the head of the incoming Pull request
*/
async function findPullRequest(
repositoryInfo: RepositoryInfo,
headRepo: string,
headBranch: string,
headSha: string
): Promise<rest.PullsListResponseItem | null> {
// Finds Pull request for this workflow run
core.info(
`\nFinding PR request id for: owner: ${repositoryInfo.owner}, Repo:${repositoryInfo.repo},` +
` Head:${headRepo}:${headBranch}.\n`
)
const pullRequests = await repositoryInfo.octokit.pulls.list({
owner: repositoryInfo.owner,
repo: repositoryInfo.repo,
head: `${headRepo}:${headBranch}`
})
for (const pullRequest of pullRequests.data) {
core.info(
`\nComparing: ${pullRequest.number} sha: ${pullRequest.head.sha} with expected: ${headSha}.\n`
)
if (pullRequest.head.sha === headSha) {
core.info(`\nFound PR: ${pullRequest.number}\n`)
return pullRequest
}
}
core.info(`\nCould not find the PR for this build :(\n`)
return null
}
/**
* Finds pull request id for the run item.
* @param repositoryInfo - information about the repository used
* @param runItem - run Item that the pull request should be found for
* @return pull request number to notify (or undefined if not found)
*/
async function findPullRequestForRunItem(
repositoryInfo: RepositoryInfo,
runItem: rest.ActionsListWorkflowRunsResponseWorkflowRunsItem
): Promise<number | undefined> {
const pullRequest = await findPullRequest(
repositoryInfo,
runItem.head_repository.owner.login,
runItem.head_branch,
runItem.head_sha
)
if (pullRequest) {
return pullRequest.number
}
return undefined
}
/**
* Maps found workflow runs into groups - filters out the workflows that are not eligible for cancelling
* (depends on cancel Mode) and assigns each workflow to groups - where workflow runs from the
* same group are put together in one array - in a map indexed by the source group id.
*
* @param repositoryInfo - information about the repository used
* @param sourceWorkflowId - workflow id to act on
* @param triggeringRunInfo - information about the workflow that triggered the run
* @param cancelMode - cancel mode to use
* @param cancelFutureDuplicates - whether to cancel future duplicates
* @param jobNameRegexps - regexps for job names
* @param skipEventTypes - array of event names to skip
* @param selfPreservation - whether the run will cancel itself if requested
* @param selfRunId - my own run id
* @param workflowRuns - map of workflow runs found
* @parm maps with string key and array of run items as value. The key might be
* * source group id (allDuplicates mode)
* * matching job name (allDuplicatedMatchingJobNames mode)
*/
async function filterAndMapWorkflowRunsToGroups(
repositoryInfo: RepositoryInfo,
sourceWorkflowId: string | number,
triggeringRunInfo: TriggeringRunInfo,
cancelMode: CancelMode,
cancelFutureDuplicates: boolean,
jobNameRegexps: string[],
skipEventTypes: string[],
selfRunId: number,
selfPreservation: boolean,
workflowRuns: Map<
number,
rest.ActionsListWorkflowRunsResponseWorkflowRunsItem
>
): Promise<
Map<string, rest.ActionsListWorkflowRunsResponseWorkflowRunsItem[]>
> {
const mapOfWorkflowRunCandidates = new Map()
for (const [key, runItem] of workflowRuns) {
core.info(
`\nChecking run number: ${key} RunId: ${runItem.id} Url: ${runItem.url} Status ${runItem.status}` +
` Created at ${runItem.created_at}\n`
)
if (runItem.id === selfRunId && selfPreservation) {
core.info(
`\nI have self-preservation built in. I refuse to cancel myself :)\n`
)
continue
}
await checkCandidateForCancelling(
repositoryInfo,
sourceWorkflowId,
runItem,
triggeringRunInfo,
cancelMode,
cancelFutureDuplicates,
jobNameRegexps,
skipEventTypes,
mapOfWorkflowRunCandidates
)
}
return mapOfWorkflowRunCandidates
}
/**
* Add specified comment to Pull Request
* @param repositoryInfo - information about the repository used
* @param pullRequestNumber - number of pull request
* @param comment - comment to add
*/
async function addCommentToPullRequest(
repositoryInfo: RepositoryInfo,
pullRequestNumber: number,
comment: string
): Promise<void> {
core.info(`\nNotifying PR: ${pullRequestNumber} with '${comment}'.\n`)
await repositoryInfo.octokit.issues.createComment({
owner: repositoryInfo.owner,
repo: repositoryInfo.repo,
// eslint-disable-next-line @typescript-eslint/camelcase
issue_number: pullRequestNumber,
body: comment
})
}
/**
* Notifies PR about cancelling
* @param repositoryInfo - information about the repository used
* @param selfRunId - my own run id
* @param pullRequestNumber - number of pull request
* @param reason reason for cancelling
*/
async function notifyPR(
repositoryInfo: RepositoryInfo,
selfRunId: number,
pullRequestNumber: number,
reason: string
): Promise<void> {
const selfWorkflowRunUrl =
`https://github.com/${repositoryInfo.owner}/${repositoryInfo.repo}` +
`/actions/runs/${selfRunId}`
await addCommentToPullRequest(
repositoryInfo,
pullRequestNumber,
`[The Workflow run](${selfWorkflowRunUrl}) is cancelling this PR. ${reason}`
)
}
/**
* Cancels all runs in the specified group of runs.
* @param repositoryInfo - information about the repository used
* @param sortedRunItems - items sorted in descending order (descending order by created_at)
* @param notifyPRCancel - whether to notify the PR when cancelling
* @param selfRunId - what is the self run id
* @param sourceGroupId - what is the source group id
* @param reason - reason for cancelling
*/
async function cancelAllRunsInTheGroup(
repositoryInfo: RepositoryInfo,
sortedRunItems: rest.ActionsListWorkflowRunsResponseWorkflowRunsItem[],
notifyPRCancel: boolean,
selfRunId: number,
sourceGroupId: string,
reason: string
): Promise<number[]> {
core.info(
`\n###### Cancelling runs for ${sourceGroupId} starting from the most recent ##########\n` +
`\n Number of runs to cancel: ${sortedRunItems.length}\n`
)
const cancelledRuns: number[] = []
for (const runItem of sortedRunItems) {
if (notifyPRCancel && runItem.event === 'pull_request') {
const pullRequestNumber = await findPullRequestForRunItem(
repositoryInfo,
runItem
)
if (pullRequestNumber !== undefined) {
core.info(
`\nNotifying PR: ${pullRequestNumber} (runItem: ${runItem}) with: ${reason}\n`
)
await notifyPR(repositoryInfo, selfRunId, pullRequestNumber, reason)
}
}
core.info(`\nCancelling run: ${runItem.id}.\n`)
await cancelRun(repositoryInfo, runItem.id)
cancelledRuns.push(runItem.id)
}
core.info(
`\n###### Finished cancelling runs for ${sourceGroupId} ##########\n`
)
return cancelledRuns
}
/**
* Cancels found runs per group. It takes all the found groups, sorts them
* descending according to create date in each of the groups and cancels them in that order -
* optionally skipping the first found run in each source group in case of duplicate cancelling.
*
* @param repositoryInfo - information about the repository used
* @param mapOfWorkflowRunCandidatesCandidatesToCancel map of all workflow run candidates
* @param cancelMode - cancel mode
* @param cancelFutureDuplicates - whether to cancel future duplicates
* @param notifyPRCancel - whether to notify PRs with comments
* @param selfRunId - self run Id
* @param reason - reason for cancelling
*/
async function cancelTheRunsPerGroup(
repositoryInfo: RepositoryInfo,
mapOfWorkflowRunCandidatesCandidatesToCancel: Map<
string,
rest.ActionsListWorkflowRunsResponseWorkflowRunsItem[]
>,
cancelMode: CancelMode,
cancelFutureDuplicates: boolean,
notifyPRCancel: boolean,
selfRunId: number,
reason: string
): Promise<number[]> {
const cancelledRuns: number[] = []
for (const [
groupId,
candidatesArray
] of mapOfWorkflowRunCandidatesCandidatesToCancel) {
// Sort from most recent date - this way we always kill current one at the end (if we kill it at all)
const sortedRunItems = candidatesArray.sort((runItem1, runItem2) =>
runItem2.created_at.localeCompare(runItem1.created_at)
)
if (sortedRunItems.length > 0) {
if (
(cancelMode === CancelMode.DUPLICATES && cancelFutureDuplicates) ||
cancelMode === CancelMode.ALL_DUPLICATES ||
cancelMode === CancelMode.ALL_DUPLICATED_NAMED_JOBS
) {
core.info(
`\nSkipping the first run (${sortedRunItems[0].id}) of all the matching ` +
`duplicates for '${groupId}'. This one we are going to leave in peace!\n`
)
sortedRunItems.shift()
}
if (sortedRunItems.length === 0) {
core.info(`\nNo duplicates to cancel for ${groupId}!\n`)
continue
}
cancelledRuns.push(
...(await cancelAllRunsInTheGroup(
repositoryInfo,
sortedRunItems,
notifyPRCancel,
selfRunId,
groupId,
reason
))
)
} else {
core.info(
`\n###### There are no runs to cancel for ${groupId} ##########\n`
)
}
}
return cancelledRuns
}
/**
* Find and cancels runs based on the criteria chosen.
* @param repositoryInfo - information about the repository used
* @param selfRunId - number of own run id
* @param sourceWorkflowId - workflow id to act on
* @param triggeringRunInfo - information about the workflow that triggered the run
* @param cancelMode - cancel mode used
* @param cancelFutureDuplicates - whether to cancel future duplicates for duplicate cancelling
* @param notifyPRCancel - whether to notify in PRs about cancelling
* @param notifyPRMessageStart - whether to notify PRs when the action starts
* @param jobNameRegexps - array of regular expressions to match hob names in case of named modes
* @param skipEventTypes - array of event names that should be skipped
* @param reason - reason for cancelling
* @param selfPreservation - whether the run will cancel itself if requested
* @return array of canceled workflow run ids
*/
async function findAndCancelRuns(
repositoryInfo: RepositoryInfo,
selfRunId: number,
sourceWorkflowId: string | number,
triggeringRunInfo: TriggeringRunInfo,
cancelMode: CancelMode,
cancelFutureDuplicates: boolean,
notifyPRCancel: boolean,
notifyPRMessageStart: string,
jobNameRegexps: string[],
skipEventTypes: string[],
reason: string,
selfPreservation: boolean
): Promise<number[]> {
const statusValues = ['queued', 'in_progress']
const workflowRuns = await getWorkflowRunsMatchingCriteria(
repositoryInfo,
sourceWorkflowId,
statusValues,
cancelMode,
triggeringRunInfo
)
const mapOfWorkflowRunCandidatesCandidatesToCancel = await filterAndMapWorkflowRunsToGroups(
repositoryInfo,
sourceWorkflowId,
triggeringRunInfo,
cancelMode,
cancelFutureDuplicates,
jobNameRegexps,
skipEventTypes,
selfRunId,
selfPreservation,
workflowRuns
)
return await cancelTheRunsPerGroup(
repositoryInfo,
mapOfWorkflowRunCandidatesCandidatesToCancel,
cancelMode,
cancelFutureDuplicates,
notifyPRCancel,
selfRunId,
reason
)
}
/**
* Returns environment variable that is required - throws error if it is not defined.
* @param key key for the env variable
* @return value of the env variable
*/
function getRequiredEnv(key: string): string {
const value = process.env[key]
if (value === undefined) {
const message = `${key} was not defined.`
throw new Error(message)
}
return value
}
/**
* Gets source run using of the runId - if this is a workflow run, it returns the information about the source run
* @param repositoryInfo - information about the repository used
* @param runId - run id of the run to check
* @return information about the triggering run
*/
async function getTriggeringRunInfo(
repositoryInfo: RepositoryInfo,
runId: number
): Promise<TriggeringRunInfo> {
const reply = await repositoryInfo.octokit.actions.getWorkflowRun({
owner: repositoryInfo.owner,
repo: repositoryInfo.repo,
// eslint-disable-next-line @typescript-eslint/camelcase
run_id: runId
})
const sourceRun = reply.data
core.info(
`Source workflow: Head repo: ${sourceRun.head_repository.full_name}, ` +
`Head branch: ${sourceRun.head_branch} ` +
`Event: ${sourceRun.event}, Head sha: ${sourceRun.head_sha}, url: ${sourceRun.url}`
)
let pullRequest: rest.PullsListResponseItem | null = null
if (sourceRun.event === 'pull_request') {
pullRequest = await findPullRequest(
repositoryInfo,
sourceRun.head_repository.owner.login,
sourceRun.head_branch,
sourceRun.head_sha
)
}
return {
workflowId: retrieveWorkflowIdFromUrl(reply.data.workflow_url),
runId,
headRepo: reply.data.head_repository.full_name,
headBranch: reply.data.head_branch,
headSha: reply.data.head_sha,
mergeCommitSha: pullRequest ? pullRequest.merge_commit_sha : null,
pullRequest: pullRequest ? pullRequest : null,
eventName: reply.data.event
}
}
/**
* Performs the actual cancelling.
*
* @param repositoryInfo - information about the repository used
* @param selfRunId - number of own run id
* @param sourceWorkflowId - id of the workflow to act on
* @param triggeringRunInfo - information about the workflow that triggered the run
* @param cancelMode - cancel mode used
* @param notifyPRCancel - whether to notify in PRs about cancelling
* @param notifyPRCancelMessage - message to send when cancelling the PR (overrides default message
* generated automatically)
* @param notifyPRMessageStart - whether to notify PRs when the action starts
* @param jobNameRegexps - array of regular expressions to match hob names in case of named modes
* @param skipEventTypes - array of event names that should be skipped
* @param cancelFutureDuplicates - whether to cancel future duplicates for duplicate cancelling
* @param selfPreservation - whether the run will cancel itself if requested
*/
async function performCancelJob(
repositoryInfo: RepositoryInfo,
selfRunId: number,
sourceWorkflowId: string | number,
triggeringRunInfo: TriggeringRunInfo,
cancelMode: CancelMode,
notifyPRCancel: boolean,
notifyPRCancelMessage: string,
notifyPRMessageStart: string,
jobNameRegexps: string[],
skipEventTypes: string[],
cancelFutureDuplicates: boolean,
selfPreservation: boolean
): Promise<number[]> {
core.info(
'\n###################################################################################\n'
)
core.info(
`All parameters: owner: ${repositoryInfo.owner}, repo: ${repositoryInfo.repo}, ` +
`run id: ${triggeringRunInfo.runId}, Source workflow id: ${sourceWorkflowId}, ` +
`head repo ${triggeringRunInfo.headRepo}, headBranch: ${triggeringRunInfo.headBranch}, ` +
`sourceEventName: ${triggeringRunInfo.eventName}, ` +
`cancelMode: ${cancelMode}, jobNames: ${jobNameRegexps}`
)
core.info(
'\n###################################################################################\n'
)
let reason = ''
if (cancelMode === CancelMode.SELF) {
core.info(
`# Cancelling source run: ${triggeringRunInfo.runId} ` +
`for workflow ${triggeringRunInfo.workflowId}.`
)
reason = notifyPRCancelMessage
? notifyPRCancelMessage
: `The job has been cancelled by another workflow.`
} else if (cancelMode === CancelMode.FAILED_JOBS) {
core.info(
`# Cancel all runs for workflow ${triggeringRunInfo.workflowId} ` +
`where job names matching ${jobNameRegexps} failed.`
)
reason = `It has some failed jobs matching ${jobNameRegexps}.`
} else if (cancelMode === CancelMode.NAMED_JOBS) {
core.info(
`# Cancel all runs for workflow ${triggeringRunInfo.workflowId} ` +
`have job names matching ${jobNameRegexps}.`
)
reason = `It has jobs matching ${jobNameRegexps}.`
} else if (cancelMode === CancelMode.DUPLICATES) {
core.info(
`# Cancel duplicate runs for workflow ${triggeringRunInfo.workflowId} ` +
`for same triggering branch as own run Id.`
)
reason = `It is an earlier duplicate of ${triggeringRunInfo.workflowId} run.`
} else if (cancelMode === CancelMode.ALL_DUPLICATES) {
core.info(
`# Cancel all duplicates runs started for workflow ${triggeringRunInfo.workflowId}.`
)
reason = `It is an earlier duplicate of ${triggeringRunInfo.workflowId} run.`
} else if (cancelMode === CancelMode.ALL_DUPLICATED_NAMED_JOBS) {
core.info(
`# Cancel all duplicate named jobs matching the patterns ${jobNameRegexps}.`
)
reason = `It is an earlier duplicate of ${triggeringRunInfo.workflowId} run.`
} else {
throw Error(`Wrong cancel mode ${cancelMode}! Please correct it.`)
}
core.info(
'\n###################################################################################\n'
)
return await findAndCancelRuns(
repositoryInfo,
selfRunId,
sourceWorkflowId,
triggeringRunInfo,
cancelMode,
cancelFutureDuplicates,
notifyPRCancel,
notifyPRMessageStart,
jobNameRegexps,
skipEventTypes,
reason,
selfPreservation
)
}
/**
* Retrieves information about source workflow Id. Either from the current event or from the workflow
* nme specified. If the file name is not specified, the workflow to act on is either set to self event
* or in case of workflow_run event - to the workflow id that triggered the 'workflow_run' event.
*
* @param repositoryInfo - information about the repository used
* @param workflowFileName - optional workflow file name
* @param runId - run id of the workflow
* @param selfRunId - self run id
* @return workflow id that is associate with the workflow we are going to act on.
*/
async function retrieveWorkflowId(
repositoryInfo: RepositoryInfo,
workflowFileName: string | null,
runId: number,
selfRunId: number
): Promise<string | number> {
let workflowId
if (workflowFileName) {
workflowId = workflowFileName
core.info(
`\nFinding runs for another workflow found by ${workflowFileName} name: ${workflowId}\n`
)
} else {
workflowId = await getWorkflowId(repositoryInfo, runId)
if (runId === selfRunId) {
core.info(`\nFinding runs for my own workflow ${workflowId}\n`)
} else {
core.info(`\nFinding runs for source workflow ${workflowId}\n`)
}
}
return workflowId
}
/**
* Sets output but also prints the output value in the logs.
*
* @param name name of the output
* @param value value of the output
*/
function verboseOutput(name: string, value: string): void {
core.info(`Setting output: ${name}: ${value}`)
core.setOutput(name, value)
}
/**
* Performs sanity check of the parameters passed. Some of the parameter combinations do not work so they
* are verified and in case od unexpected combination found, appropriate error is raised.
*
* @param eventName - name of the event to act on
* @param runId - run id of the triggering event
* @param selfRunId - our own run id
* @param cancelMode - cancel mode used
* @param cancelFutureDuplicates - whether future duplicate cancelling is enabled
* @param jobNameRegexps - array of regular expression of job names
*/
function performSanityChecks(
eventName: string,
runId: number,
selfRunId: number,
cancelMode: CancelMode,
cancelFutureDuplicates: boolean,
jobNameRegexps: string[]
): void {
if (
eventName === 'workflow_run' &&
runId === selfRunId &&
cancelMode === CancelMode.DUPLICATES
) {
throw Error(
`You cannot run "workflow_run" in ${cancelMode} cancelMode without "sourceId" input.` +
'It will likely not work as you intended - it will cancel runs which are not duplicates!' +
'See the docs for details.'
)
}
if (
jobNameRegexps.length > 0 &&
[
CancelMode.DUPLICATES,
CancelMode.SELF,
CancelMode.ALL_DUPLICATES
].includes(cancelMode)
) {
throw Error(
`You cannot specify jobNamesRegexps on ${cancelMode} cancelMode.`
)
}
if (
jobNameRegexps.length === 0 &&
[
CancelMode.NAMED_JOBS,
CancelMode.FAILED_JOBS,
CancelMode.ALL_DUPLICATED_NAMED_JOBS
].includes(cancelMode)
) {
throw Error(`You must specify jobNamesRegexps on ${cancelMode} cancelMode.`)
}
if (
(cancelMode === CancelMode.ALL_DUPLICATES ||
cancelMode === CancelMode.ALL_DUPLICATED_NAMED_JOBS) &&
!cancelFutureDuplicates
) {
throw Error(
`The ${cancelMode} cancelMode has to have cancelFutureDuplicates set to true.`
)
}
}
/**
* Produces basic outputs for the action. This does not include cancelled workflow run id - those are
* set after cancelling is done.
*
* @param triggeringRunInfo
*/
function produceBasicOutputs(triggeringRunInfo: TriggeringRunInfo): void {
verboseOutput('sourceHeadRepo', triggeringRunInfo.headRepo)
verboseOutput('sourceHeadBranch', triggeringRunInfo.headBranch)
verboseOutput('sourceHeadSha', triggeringRunInfo.headSha)
verboseOutput('sourceEvent', triggeringRunInfo.eventName)
verboseOutput(
'pullRequestNumber',
triggeringRunInfo.pullRequest
? triggeringRunInfo.pullRequest.number.toString()
: ''
)
verboseOutput(
'mergeCommitSha',
triggeringRunInfo.mergeCommitSha ? triggeringRunInfo.mergeCommitSha : ''
)
verboseOutput(
'targetCommitSha',
triggeringRunInfo.mergeCommitSha
? triggeringRunInfo.mergeCommitSha
: triggeringRunInfo.headSha
)
}
/**
* Notifies the PR that the action has started.
*
* @param repositoryInfo information about the repository
* @param triggeringRunInfo information about the triggering workflow
* @param selfRunId self run id
* @param notifyPRMessageStart whether to notify about the start of the action
*/
async function notifyActionStart(
repositoryInfo: RepositoryInfo,
triggeringRunInfo: TriggeringRunInfo,
selfRunId: number,
notifyPRMessageStart: string
): Promise<void> {
if (notifyPRMessageStart && triggeringRunInfo.pullRequest) {
const selfWorkflowRunUrl =
`https://github.com/${repositoryInfo.owner}/${repositoryInfo.repo}` +
`/actions/runs/${selfRunId}`
await repositoryInfo.octokit.issues.createComment({
owner: repositoryInfo.owner,
repo: repositoryInfo.repo,
// eslint-disable-next-line @typescript-eslint/camelcase
issue_number: triggeringRunInfo.pullRequest.number,
body: `${notifyPRMessageStart} [The workflow run](${selfWorkflowRunUrl})`
})
}
}
/**
* Main run method that does everything :)
*/
async function run(): Promise<void> {
const token = core.getInput('token', {required: true})
const octokit = new github.GitHub(token)
const selfRunId = parseInt(getRequiredEnv('GITHUB_RUN_ID'))
const repository = getRequiredEnv('GITHUB_REPOSITORY')
const eventName = getRequiredEnv('GITHUB_EVENT_NAME')
const cancelMode =
(core.getInput('cancelMode') as CancelMode) || CancelMode.DUPLICATES
const notifyPRCancel =
(core.getInput('notifyPRCancel') || 'false').toLowerCase() === 'true'
const notifyPRCancelMessage = core.getInput('notifyPRCancelMessage')
const notifyPRMessageStart = core.getInput('notifyPRMessageStart')
const sourceRunId = parseInt(core.getInput('sourceRunId')) || selfRunId
const jobNameRegexpsString = core.getInput('jobNameRegexps')
const selfPreservation =
(core.getInput('selfPreservation') || 'true').toLowerCase() === 'true'
const cancelFutureDuplicates =
(core.getInput('cancelFutureDuplicates') || 'true').toLowerCase() === 'true'
const jobNameRegexps = jobNameRegexpsString
? JSON.parse(jobNameRegexpsString)
: []
const skipEventTypesString = core.getInput('skipEventTypes')
const skipEventTypes = skipEventTypesString
? JSON.parse(skipEventTypesString)
: []
const workflowFileName = core.getInput('workflowFileName')
const [owner, repo] = repository.split('/')
const repositoryInfo: RepositoryInfo = {
octokit,
owner,
repo
}
core.info(
`\nGetting workflow id for source run id: ${sourceRunId}, owner: ${owner}, repo: ${repo},` +
` skipEventTypes: ${skipEventTypes}\n`
)
const sourceWorkflowId = await retrieveWorkflowId(
repositoryInfo,
workflowFileName,
sourceRunId,
selfRunId
)
performSanityChecks(
eventName,
sourceRunId,
selfRunId,
cancelMode,
cancelFutureDuplicates,
jobNameRegexps
)
core.info(
`Repository: ${repository}, Owner: ${owner}, Repo: ${repo}, ` +
`Event name: ${eventName}, CancelMode: ${cancelMode}, ` +
`sourceWorkflowId: ${sourceWorkflowId}, sourceRunId: ${sourceRunId}, selfRunId: ${selfRunId}, ` +
`jobNames: ${jobNameRegexps}`
)
const triggeringRunInfo = await getTriggeringRunInfo(
repositoryInfo,
sourceRunId
)
produceBasicOutputs(triggeringRunInfo)
await notifyActionStart(
repositoryInfo,
triggeringRunInfo,
selfRunId,
notifyPRMessageStart
)
const cancelledRuns = await performCancelJob(
repositoryInfo,
selfRunId,
sourceWorkflowId,
triggeringRunInfo,
cancelMode,
notifyPRCancel,
notifyPRCancelMessage,
notifyPRMessageStart,
jobNameRegexps,
skipEventTypes,
cancelFutureDuplicates,
selfPreservation
)
verboseOutput('cancelledRuns', JSON.stringify(cancelledRuns))
}
run()
.then(() =>
core.info('\n############### Cancel complete ##################\n')
)
.catch(e => core.setFailed(e.message))