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

+58
View File
@@ -94,3 +94,61 @@
order: 2;
margin-left: auto;
}
.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);
text-decoration: none;
}
.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;
}
.ui.vertical.menu .artifact-preview-item {
padding-left: calc(1em + var(--depth, 0) * 1.25em);
}
+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 */
+1
View File
@@ -77,6 +77,7 @@ function initRepositoryActionsView() {
showLogSeconds: el.getAttribute('data-locale-show-log-seconds'),
showFullScreen: el.getAttribute('data-locale-show-full-screen'),
downloadLogs: el.getAttribute('data-locale-download-logs'),
downloadFile: el.getAttribute('data-locale-download-file'),
copyOutput: el.getAttribute('data-locale-copy-output'),
status: {
unknown: el.getAttribute('data-locale-status-unknown'),
+4
View File
@@ -53,6 +53,10 @@ export function initExternalRenderIframe(iframe: HTMLIFrameElement) {
if (cmd === 'resize') {
iframe.style.height = `${e.data.iframeHeight}px`;
} else if (cmd === 'open-link') {
if (!navigator.userActivation.isActive) {
console.error(`iframe attempted to open link without user activation: ${e.data.openLink}`);
return;
}
navigateToIframeLink(e.data.openLink, e.data.anchorTarget);
} else {
throw new Error(`Unknown gitea iframe cmd: ${cmd}`);
+1
View File
@@ -88,4 +88,5 @@ export type ActionsArtifact = {
size: number;
status: ActionsArtifactStatus;
expiresUnix: number;
previewLink?: string;
};