feat(actions): add job summaries (GITHUB_STEP_SUMMARY) (#37500)

- Add GitHub-style Actions **job summaries** support
(`GITHUB_STEP_SUMMARY` / `workflow/SUMMARY.md`) and render them on the
run Summary view.
- Store uploaded summaries internally in the DB (not as downloadable
artifacts).
- Add runtime-token endpoint for runners to upload summaries:
- `PUT
/api/actions_pipeline/_apis/pipelines/workflows/{run_id}/jobs/{job_id}/summary`
- Advertise support to runners via `RunnerService.Declare` response
header:
  - `X-Gitea-Actions-Capabilities: job-summary`
- Devtest: extend `/devtest/repo-action-view/...` to include mock
`jobSummaries` for previewing UI rendering.

## Compatibility
- New Gitea + old runner: no summary upload → UI shows nothing (no
behavior change)
- New runner + old Gitea: capability not advertised → runner skips
upload (no behavior change)

## Screenshot:

<img width="2017" height="729"
src="https://github.com/user-attachments/assets/31f8b945-50c4-40e1-9f40-382901a53013"
/>


Fixes #23721
PR on gitea-runner https://gitea.com/gitea/runner/pulls/917

---------

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
This commit is contained in:
bircni
2026-06-08 21:11:00 +02:00
committed by GitHub
parent b1c088e9cf
commit 3b1e75764e
18 changed files with 683 additions and 23 deletions
+1
View File
@@ -128,6 +128,7 @@ export function createEmptyActionsRun(): ActionsRun {
triggerEvent: '',
pullRequest: null,
jobs: [] as Array<ActionsJob>,
jobSummaries: [],
commit: {
localeCommit: '',
localePushedBy: '',
+80 -17
View File
@@ -22,6 +22,11 @@ const props = defineProps<{
const locale = props.locale;
const store = createActionRunViewStore(props.actionsViewUrl);
const {currentRun: run, runArtifacts: artifacts} = toRefs(store.viewData);
const visibleJobSummaries = computed(() => {
const summaries = run.value.jobSummaries || [];
if (!props.jobId) return summaries;
return summaries.filter((summary) => summary.jobId === props.jobId);
});
type JobListItem = {
job: ActionsJob;
@@ -277,19 +282,35 @@ async function deleteArtifact(name: string) {
</div>
<div class="action-view-right">
<ActionRunSummaryView
v-if="!props.jobId"
:store="store"
:locale="locale"
:artifact-count="artifacts.length"
/>
<ActionRunJobView
v-else
:store="store"
:locale="locale"
:actions-view-url="props.actionsViewUrl"
:job-id="props.jobId"
/>
<div class="action-view-right-panel">
<ActionRunSummaryView
v-if="!props.jobId"
:store="store"
:locale="locale"
:artifact-count="artifacts.length"
/>
<ActionRunJobView
v-else
:store="store"
:locale="locale"
:actions-view-url="props.actionsViewUrl"
:job-id="props.jobId"
/>
</div>
<div v-if="visibleJobSummaries.length" class="action-view-right-panel job-summary-section">
<div class="job-summary-section-header">
{{ locale.jobSummaries }}
</div>
<div class="job-summary-list">
<div v-for="s in visibleJobSummaries" :key="s.jobId" class="job-summary-item">
<div class="job-summary-header">
<strong class="gt-ellipsis">{{ s.jobName || `Job ${s.jobId}` }}</strong>
</div>
<!-- eslint-disable-next-line vue/no-v-html -->
<div class="markup job-summary-body" v-html="s.summaryHTML"/>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -463,25 +484,32 @@ async function deleteArtifact(name: string) {
width: 70%;
display: flex;
flex-direction: column;
gap: 12px;
}
.action-view-right-panel {
border: 1px solid var(--color-console-border);
border-radius: var(--border-radius);
background: var(--color-console-bg);
display: flex;
flex-direction: column;
min-height: 0;
}
/* begin fomantic button overrides */
.action-view-right .ui.button,
.action-view-right .ui.button:focus {
.action-view-right-panel .ui.button,
.action-view-right-panel .ui.button:focus {
background: transparent;
color: var(--color-console-fg-subtle);
}
.action-view-right .ui.button:hover {
.action-view-right-panel .ui.button:hover {
background: var(--color-console-hover-bg);
color: var(--color-console-fg);
}
.action-view-right .ui.button:active {
.action-view-right-panel .ui.button:active {
background: var(--color-console-active-bg);
color: var(--color-console-fg);
}
@@ -499,4 +527,39 @@ async function deleteArtifact(name: string) {
max-width: none;
}
}
.job-summary-section {
overflow: hidden;
}
.job-summary-section-header {
padding: 12px;
border-bottom: 1px solid var(--color-console-border);
background: var(--color-console-bg);
color: var(--color-console-fg);
font-weight: var(--font-weight-semibold);
}
.job-summary-list {
padding: 12px;
display: flex;
flex-direction: column;
gap: 12px;
}
.job-summary-item {
padding: 12px;
border-radius: var(--border-radius);
background: var(--color-console-hover-bg);
border: 1px solid var(--color-console-border);
}
.job-summary-header {
color: var(--color-console-fg);
margin-bottom: 8px;
}
.job-summary-body {
color: var(--color-console-fg);
}
</style>