mirror of https://github.com/go-gitea/gitea.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
154 lines
5.1 KiB
154 lines
5.1 KiB
import {createElementFromAttrs} from '../utils/dom.ts'; |
|
import {renderAnsi} from '../render/ansi.ts'; |
|
import {reactive} from 'vue'; |
|
import type {ActionsArtifact, ActionsJob, ActionsRun, ActionsRunStatus} from '../modules/gitea-actions.ts'; |
|
import type {IntervalId} from '../types.ts'; |
|
import {POST} from '../modules/fetch.ts'; |
|
|
|
// How GitHub Actions logs work: |
|
// * Workflow command outputs log commands like "::group::the-title", "::add-matcher::...." |
|
// * Workflow runner parses and processes the commands to "##[group]", apply "matchers", hide secrets, etc. |
|
// * The reported logs are the processed logs. |
|
// HOWEVER: Gitea runner does not completely process those commands. Many works are done by the frontend at the moment. |
|
const LogLinePrefixCommandMap: Record<string, LogLineCommandName> = { |
|
'::group::': 'group', |
|
'##[group]': 'group', |
|
'::endgroup::': 'endgroup', |
|
'##[endgroup]': 'endgroup', |
|
|
|
'##[error]': 'error', |
|
'[command]': 'command', |
|
|
|
// https://github.com/actions/toolkit/blob/master/docs/commands.md |
|
// https://github.com/actions/runner/blob/main/docs/adrs/0276-problem-matchers.md#registration |
|
'::add-matcher::': 'hidden', |
|
'##[add-matcher]': 'hidden', |
|
'::remove-matcher': 'hidden', // it has arguments |
|
}; |
|
|
|
export type LogLine = { |
|
index: number; |
|
timestamp: number; |
|
message: string; |
|
}; |
|
|
|
export type LogLineCommandName = 'group' | 'endgroup' | 'command' | 'error' | 'hidden'; |
|
export type LogLineCommand = { |
|
name: LogLineCommandName, |
|
prefix: string, |
|
}; |
|
|
|
export function parseLogLineCommand(line: LogLine): LogLineCommand | null { |
|
// TODO: in the future it can be refactored to be a general parser that can parse arguments, drop the "prefix match" |
|
for (const prefix of Object.keys(LogLinePrefixCommandMap)) { |
|
if (line.message.startsWith(prefix)) { |
|
return {name: LogLinePrefixCommandMap[prefix], prefix}; |
|
} |
|
} |
|
return null; |
|
} |
|
|
|
export function createLogLineMessage(line: LogLine, cmd: LogLineCommand | null) { |
|
const logMsgAttrs = {class: 'log-msg'}; |
|
if (cmd?.name) logMsgAttrs.class += ` log-cmd-${cmd?.name}`; // make it easier to add styles to some commands like "error" |
|
|
|
// TODO: for some commands (::group::), the "prefix removal" works well, for some commands with "arguments" (::remove-matcher ...::), |
|
// it needs to do further processing in the future (fortunately, at the moment we don't need to handle these commands) |
|
const msgContent = cmd ? line.message.substring(cmd.prefix.length) : line.message; |
|
|
|
const logMsg = createElementFromAttrs('span', logMsgAttrs); |
|
logMsg.innerHTML = renderAnsi(msgContent); |
|
return logMsg; |
|
} |
|
|
|
export function createEmptyActionsRun(): ActionsRun { |
|
return { |
|
link: '', |
|
title: '', |
|
titleHTML: '', |
|
status: '' as ActionsRunStatus, // do not show the status before initialized, otherwise it would show an incorrect "error" icon |
|
canCancel: false, |
|
canApprove: false, |
|
canRerun: false, |
|
canRerunFailed: false, |
|
canDeleteArtifact: false, |
|
done: false, |
|
workflowID: '', |
|
workflowLink: '', |
|
isSchedule: false, |
|
duration: '', |
|
triggeredAt: 0, |
|
triggerEvent: '', |
|
jobs: [] as Array<ActionsJob>, |
|
commit: { |
|
localeCommit: '', |
|
localePushedBy: '', |
|
shortSHA: '', |
|
link: '', |
|
pusher: { |
|
displayName: '', |
|
link: '', |
|
}, |
|
branch: { |
|
name: '', |
|
link: '', |
|
isDeleted: false, |
|
}, |
|
}, |
|
}; |
|
} |
|
|
|
export function createActionRunViewStore(actionsUrl: string, runId: number) { |
|
let loadingAbortController: AbortController | null = null; |
|
let intervalID: IntervalId | null = null; |
|
const viewData = reactive({ |
|
currentRun: createEmptyActionsRun(), |
|
runArtifacts: [] as Array<ActionsArtifact>, |
|
}); |
|
const loadCurrentRun = async () => { |
|
if (loadingAbortController) return; |
|
const abortController = new AbortController(); |
|
loadingAbortController = abortController; |
|
try { |
|
const url = `${actionsUrl}/runs/${runId}`; |
|
const resp = await POST(url, {signal: abortController.signal, data: {}}); |
|
const runResp = await resp.json(); |
|
if (loadingAbortController !== abortController) return; |
|
|
|
viewData.runArtifacts = runResp.artifacts || []; |
|
viewData.currentRun = runResp.state.run; |
|
// clear the interval timer if the job is done |
|
if (viewData.currentRun.done && intervalID) { |
|
clearInterval(intervalID); |
|
intervalID = null; |
|
} |
|
} catch (e) { |
|
// avoid network error while unloading page, and ignore "abort" error |
|
if (e instanceof TypeError || abortController.signal.aborted) return; |
|
throw e; |
|
} finally { |
|
if (loadingAbortController === abortController) loadingAbortController = null; |
|
} |
|
}; |
|
|
|
return reactive({ |
|
viewData, |
|
|
|
async startPollingCurrentRun() { |
|
await loadCurrentRun(); |
|
intervalID = setInterval(() => loadCurrentRun(), 1000); |
|
}, |
|
async forceReloadCurrentRun() { |
|
loadingAbortController?.abort(); |
|
loadingAbortController = null; |
|
await loadCurrentRun(); |
|
}, |
|
stopPollingCurrentRun() { |
|
if (!intervalID) return; |
|
clearInterval(intervalID); |
|
intervalID = null; |
|
}, |
|
}); |
|
} |
|
|
|
export type ActionRunViewStore = ReturnType<typeof createActionRunViewStore>;
|
|
|