Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft: Display Github Actions results as plain HTML list #17755

Closed
wants to merge 13 commits into from
2 changes: 2 additions & 0 deletions options/locale/locale_ja-JP.ini
Original file line number Diff line number Diff line change
Expand Up @@ -1621,6 +1621,8 @@ activity.git_stats_and_deletions=、
activity.git_stats_deletion_1=%d行削除
activity.git_stats_deletion_n=%d行削除

actions=Actions

search=検索
search.search_repo=リポジトリを検索
search.fuzzy=あいまい
Expand Down
26 changes: 26 additions & 0 deletions routers/web/repo/actions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package repo

import (
"net/http"

"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
)

const (
tplActions base.TplName = "repo/actions"
)

// Actions Github Actions for gitea
func Actions(ctx *context.Context) {
/*ctx.Data["Title"] = ctx.Tr("repo.activity")
ctx.Data["PageIsActivity"] = true

ctx.Data["Period"] = ctx.Params("period")*/

ctx.HTML(http.StatusOK, tplActions)
}
4 changes: 4 additions & 0 deletions routers/web/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -918,6 +918,10 @@ func RegisterRoutes(m *web.Route) {
m.Get("/{period}", repo.Activity)
}, context.RepoRef(), repo.MustBeNotEmpty, context.RequireRepoReaderOr(models.UnitTypePullRequests, models.UnitTypeIssues, models.UnitTypeReleases))

m.Group("/actions", func() {
m.Get("", repo.Actions)
}, context.RepoRef(), repo.MustBeNotEmpty)

m.Group("/activity_author_data", func() {
m.Get("", repo.ActivityAuthors)
m.Get("/{period}", repo.ActivityAuthors)
Expand Down
271 changes: 271 additions & 0 deletions templates/repo/actions.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
{{template "base/head" .}}
<div class="page-content repository commits">
{{template "repo/header" .}}
<div class="ui container">
<ul id="displayJobs"></ul>

<!-- display list of timeline -->
<div id="displayTimeline"></div>
</div>

<script>
const ghHostApiUrl = 'http://localhost:5000';
const owner = 'runner';
const repo = 'server';

let jobs = [];

const source = new EventSource(`${ghHostApiUrl}/${owner}/${repo}/_apis/v1/Message/event?filter=**`);
source.addEventListener('job', (ev) => {
const je = JSON.parse(ev.data);
const x = je.job;

const r = jobs.filter((j) => j.requestId < x.requestId);
r.unshift(x);
jobs = r;

for (const job of jobs) {
const lineContents = [`requestId: ${job.requestId}`, `runid: ${job.runid}`, `name: ${job.name}`, `repo: ${job.repo}`];
for (const lineContent of lineContents) {
const liDOM = document.createElement('li');
const jobContent = document.createTextNode(lineContent);
liDOM.appendChild(jobContent);
document.getElementById('displayJobs').appendChild(liDOM);
}
const hrDOM = document.createElement('hr');
document.getElementById('displayJobs').appendChild(hrDOM);
}
});

const apiUrl = `${ghHostApiUrl}/${owner}/${repo}/_apis/v1/Message`;

(async () => {
const newjobs = JSON.parse((await (await fetch(apiUrl, {})).text()));
let sjobs = newjobs.sort((a, b) => b.requestId - a.requestId);

if (jobs.length > 0) {
const x = jobs[jobs.length - 1];
sjobs = sjobs.filter((j) => j.requestId < x.requestId);
}

jobs = [...sjobs, ...jobs];
})();

async function listArtifacts(runid) {
const artifactUrl = `${ghHostApiUrl}/runner/host/_apis/pipelines/workflows/${runid}/artifacts`;

const response = await fetch(artifactUrl);
const body = await response.text();
return JSON.parse(body);
}

async function getContainerItems(artifactName, containerUrl) {
const resourceUrl = new URL(containerUrl);
resourceUrl.searchParams.append('itemPath', artifactName);

const response = await fetch(resourceUrl.toString());
const body = await response.text();
return JSON.parse(body);
}

function setJobs(f) {
jobs = f(jobs);
}

let timeline = [];

function setTimeline(f) {
timeline = f(timeline);
}

let artifacts = [];

function setArtifacts(f) {
artifacts = f(artifacts);
}

let title = 'Loading...';

function setTitle(f) {
title = f(title);
}

const id = '';
let errors = [];

function setErrors(f) {
errors = f(errors);
}

function getJobById(jobs, id) {
const actualId = (typeof id === 'string') ? parseInt(id) : id;
let item = null;

if (actualId !== undefined && actualId !== null) {
const x = jobs.find((x) => x.requestId === actualId) || null;

if (x !== null) {
item = {
id: x.requestId,
title: x.jobId,
description: x.timeLineId
};

return {
item,
job: x
};
}
}

return {
item: null,
job: null
};
}

async function main() {
setArtifacts((_) => []);
if (id === undefined) {
return;
}
let njobs;
const _id = Number.parseInt(id);
if (jobs.length === 0 || jobs.some((x) => x.requestId === _id)) {
njobs = await (await (await fetch(`${ghHostApiUrl}/${owner}/${repo}/_apis/v1/Message`, {})).json());
setJobs((_) => njobs);
}
const query = getJobById(njobs || jobs, id);
if (query.job.errors !== null && query.job.errors.length > 0) {
setErrors(query.job.errors);
} else {
setErrors([]);
}
const item = query.item;
const timelineId = item ? item.description : null;
if (timelineId !== null) {
const timeline = await fetch(`${ghHostApiUrl}/owner/repo/_apis/v1/Timeline/${timelineId}`, {});
if (timeline.status === 200) {
const newTimeline = await timeline.json();
if (newTimeline !== null && newTimeline.length > 1) {
setTitle(newTimeline.shift().name);
setTimeline(newTimeline);
} else {
setTitle('Unknown');
setTimeline([]);
}
} else {
setTitle((query.job.errors !== null && query.job.errors.length > 0) ? 'Failed to run' : 'Wait for workflow to run...');
setTimeline((_) => []);
}
}
if (query.job.runid !== -1) {
const artifacts = await listArtifacts(query.job.runid);
if (artifacts.value !== undefined) {
for (let i = 0; i < artifacts.count; i++) {
const element = artifacts.value[i];
const items = await getContainerItems(element.name, element.fileContainerResourceUrl);
if (items !== undefined) {
element.files = items.value;
}
}
setArtifacts((_) => artifacts.value);
}
}
}

async function listener() {
if (id !== undefined && id !== null && id.length > 0) {
const item = getJobById(jobs, id).item;
if (item !== null && item.description && item.description !== '' && item.description !== '00000000-0000-0000-0000-000000000000') {
const source = new EventSource(`${ghHostApiUrl}/${owner}/${repo}/_apis/v1/TimeLineWebConsoleLog?timelineId=${item.description}`);
try {
const missed = [];
const callback = function(timeline, e) {
const s = timeline.find((t) => t.id === e.record.stepId);
if (s !== null && s !== undefined) {
if (s.log === null) {
s.log = {
id: -1,
location: null,
content: ''
};

if (e.record.startLine > 1) {
(async () => {
const lines = await fetch(`${ghHostApiUrl}/${owner}/${repo}/_apis/v1/TimeLineWebConsoleLog/${item.description}/${e.record.stepId}`, {});
if (lines.status === 200) {
const missingLines = await lines.json();
missingLines.length = e.record.startLine - 1;
s.log.content = `${missingLines.reduce((prev, c) => `${(prev.length > 0 ? `${prev}<br/>` : '')}${c.line}`, '')}${s.log.content}`;
}
})();
}
}
if (s.log.id === -1) {
s.log.content = e.record.value.reduce((prev, c) => `${(prev.length > 0 ? `${prev}<br/>` : '')}${c}`, s.log.content);
}
return true;
}
return false;
};
source.addEventListener('log', (ev) => {
const e = JSON.parse(ev.data);
setTimeline((timeline) => {
if (callback(timeline, e)) {
return [...timeline];
}
missed.push(e);
return timeline;
});
});
source.addEventListener('timeline', (ev) => {
const e = JSON.parse(ev.data);
setTitle(e.timeline.shift().name);
setTimeline((oldtimeline) => {
const del = e.timeline.splice(0, oldtimeline.length);
for (let i = 0; i < del.length; i++) {
oldtimeline[i].result = del[i].result;
oldtimeline[i].state = del[i].state;
}
if (e.timeline.length === 0) {
// Todo Merge Timelines here
return oldtimeline;
}
const timeline = [...oldtimeline, ...e.timeline];
for (; missed.length > 0;) {
if (callback(timeline, missed[0])) {
missed.shift();
} else {
break;
}
}
return timeline;
});
});
} catch (error) {
console.error(error);
}

return (() => {
source.close();
});
}
}
return () => {};
}

main();
listener();

</script>

<style>
#displayJobs {
list-style: none;

}
</style>

</div>
{{template "base/footer" .}}
4 changes: 4 additions & 0 deletions templates/repo/header.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,10 @@
</a>
{{end}}

<a class="{{if .PageIsActivity}}active{{end}} item" href="{{.RepoLink}}/actions">
{{svg "octicon-pulse"}} {{.i18n.Tr "repo.actions"}}
</a>

{{template "custom/extra_tabs" .}}

{{if .Permission.IsAdmin}}
Expand Down