feat(actions): Add artifact preview in Actions run view (#36754)

Closes https://github.com/go-gitea/gitea/issues/33579.

Adds browser previews for Actions artifacts. Selecting an artifact opens
its file browser; selecting a file renders it in the same tab. The ZIP
download remains available separately.

Previews require sign-in and read access to the run. Text, image and PDF
files are supported; rendered HTML and JavaScript run in a sandboxed
frame and are labeled as automatically generated. The frame loads files
from a signed link that expires after an hour, because its requests
carry no session cookie. `[actions] ARTIFACT_PREVIEW_MAX_SIZE` limits
total previewable artifact size (`0` disables previews; `-1` removes the
limit); individual files also follow `[ui] MAX_DISPLAY_FILE_SIZE`.

<img width="1803" height="913" alt="image"
src="https://github.com/user-attachments/assets/a38fd704-2244-44fa-9181-c695ecbe0276"
/>

Docs: https://gitea.com/gitea/docs/pulls/533

---------

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-09-26 07:42:36 +00:00
1 parent 857d3d3df3
commit 5a56e118e4
22 files changed
+971 -68

No files matched your search

+6 -58
View File
@@ -35,6 +35,7 @@ type RepoActionViewLocale = ActionRunSummaryViewLocale & ActionRunJobViewLocale
artifactExpiresAt: string,
artifactExpiredAt: string,
confirmDeleteArtifact: string,
downloadFile: string,
workflowFile: string,
workflowFileNoPermission: string,
runDetails: string,
@@ -287,8 +288,8 @@ onBeforeUnmount(() => {
<div class="item" v-for="artifact in artifacts" :key="artifact.name">
<template v-if="artifact.status !== 'expired'">
<a
class="tw-flex-1 tw-min-w-0 flex-text-block silenced" target="_blank"
:href="buildArtifactLink(artifact.name)"
class="tw-flex-1 tw-min-w-0 flex-text-block silenced"
:href="artifact.previewLink"
:data-tooltip-content="buildArtifactTooltipHtml(artifact, locale.artifactExpiresAt)"
data-tooltip-render="html"
data-tooltip-placement="top-end"
@@ -296,6 +297,9 @@ onBeforeUnmount(() => {
<SvgIcon name="octicon-file" class="tw-text-text-light"/>
<span class="tw-flex-1 gt-ellipsis">{{ artifact.name }}</span>
</a>
<a download class="silenced" :href="buildArtifactLink(artifact.name)" :data-tooltip-content="locale.downloadFile">
<SvgIcon name="octicon-download"/>
</a>
<a v-if="run.canDeleteArtifact" class="silenced" @click="deleteArtifact(artifact.name)">
<SvgIcon name="octicon-trash"/>
</a>
@@ -373,62 +377,6 @@ onBeforeUnmount(() => {
gap: 12px;
}
/* ================ */
/* action view header */
.action-view-header {
display: flex;
flex-direction: column;
gap: 4px;
margin-top: 8px;
min-height: 50px; /* reserve the back link and title height so the body does not shift when the run data arrives */
}
.action-view-back {
display: inline-flex;
align-items: center;
align-self: flex-start;
gap: 4px;
font-size: 13px;
color: var(--color-text-light-1);
text-decoration: none;
}
.action-view-back:hover {
color: var(--color-text);
}
.action-info-summary {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.action-info-summary-title {
display: flex;
align-items: center;
gap: 0.5em;
}
.action-info-summary-title-text {
font-size: 20px;
margin: 0;
overflow-wrap: anywhere;
}
.action-info-summary-title-index {
font-size: 20px;
color: var(--color-text-light-2);
flex: 1;
}
.action-info-summary .ui.button {
margin: 0;
white-space: nowrap;
}
/* ================ */
/* action view left */