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

Cache and display build logs #2653

Merged
merged 1 commit into from
Jan 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11,339 changes: 3,133 additions & 8,206 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

55 changes: 0 additions & 55 deletions src/api/status/public/js/build-log/api.js

This file was deleted.

54 changes: 36 additions & 18 deletions src/api/status/public/js/build-log/build-header.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// eslint-disable-next-line import/no-unresolved
import ms from 'https://cdn.jsdelivr.net/npm/@esm/ms@2.1.0/index.js';

const buildHeaderTitle = document.getElementById('build-header-title');
const buildHeaderInfo = document.getElementById('build-header-info');
const buildSender = document.getElementById('build-sender');
Expand All @@ -9,31 +12,45 @@ const buildStarted = document.getElementById('build-started');
const buildDuration = document.getElementById('build-duration');
const buildPrevious = document.getElementById('previous-build');

function renderBuildInfo({ githubData, startedAt, stoppedAt, result, previous }) {
function renderBuildTimeInfo(startedDate, stoppedDate = new Date()) {
const duration = new Date(stoppedDate).getTime() - new Date(startedDate).getTime();
buildDuration.innerText = ms(duration);
buildStarted.innerText = new Date(startedDate).toUTCString();
}

function renderSender(sender) {
buildSender.href = sender.html_url;
buildSenderName.innerText = sender.login;
buildSenderImg.src = sender.avatar_url;
}

function renderSha(compare, after) {
buildGitSHA.href = compare;
buildGitSHA.innerText = after.substring(0, 7);
}

function renderBuildInfo({ isCurrent, githubData, startedDate, stoppedDate, code }) {
const { sender, after, compare } = githubData;

if (buildHeaderInfo.hidden) {
buildHeaderInfo.removeAttribute('hidden');
buildHeaderInfo.hidden = false;
}
if (previous) {

if (!isCurrent) {
buildPrevious.innerText = 'Previous Build';
}

buildHeaderTitle.innerHTML = '';
buildSender.href = githubData.sender.html_url;
buildSenderName.innerText = githubData.sender.login;
buildSenderImg.src = githubData.sender.avatar_url;
buildGitSHA.href = githubData.compare;
buildGitSHA.innerText = githubData.after.substring(0, 7);
buildResult.innerText = result === 0 ? 'Good' : 'Error';
buildStarted.innerText = new Date(startedAt).toUTCString();

const duration = new Date(stoppedAt).getTime() - new Date(startedAt).getTime();
const minutes = Math.floor(duration / 60000);
const seconds = ((duration % 60000) / 1000).toFixed(0);
buildDuration.innerText = `${minutes}m ${seconds}s`;

renderSender(sender);
renderSha(compare, after);
renderBuildTimeInfo(startedDate, stoppedDate);

buildResult.innerText = code === 0 ? 'Success' : 'Error';
}

export default function buildHeader(data) {
if (!data.building) {
export default function buildHeader(build) {
if (build && !build.stoppedDate) {
const icon = document.createElement('i');
icon.className = 'fas fa-server px-2';
buildHeaderTitle.innerHTML = '';
Expand All @@ -42,5 +59,6 @@ export default function buildHeader(data) {
buildHeaderInfo.innerHTML = '';
return;
}
renderBuildInfo(data);

renderBuildInfo(build);
}
50 changes: 50 additions & 0 deletions src/api/status/public/js/build-log/check-build-status.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Get the proper URL for the autodeployment server based on how this is
// being loaded. On staging and production, remove the `api.` subdomain.
// On localhost:1111, use localhost (drop the port).
const autodeploymentUrl = (path) =>
`//${window.location.hostname.replace('.api', '')}/deploy${path}`;

const getBuildLog = async (buildName) => {
if (!(buildName === 'current' || buildName === 'previous')) {
throw new Error(`invalid build name: ${buildName}`);
}

try {
const res = await fetch(autodeploymentUrl(`/log/${buildName}`));
if (!res.ok) {
if (res.status === 404) {
return null;
}
throw new Error('unable to get build log');
}
return res.body.getReader();
} catch (err) {
console.warn(err);
return null;
}
};

export default async () => {
try {
const res = await fetch(autodeploymentUrl('/status'));
if (!res.ok) {
throw new Error('unable to get build info');
}
const data = await res.json();

// Decorate builds with a getReader() function
if (data.previous) {
data.current.isCurrent = false;
data.previous.getReader = () => getBuildLog('previous');
}
if (data.current) {
data.current.isCurrent = true;
data.current.getReader = () => getBuildLog('current');
}

return data;
} catch (err) {
console.error(err);
return { previous: null, current: null, pending: null };
}
};
71 changes: 36 additions & 35 deletions src/api/status/public/js/build-log/check-for-build.js
Original file line number Diff line number Diff line change
@@ -1,48 +1,49 @@
/* eslint-disable consistent-return */
import { checkBuildStatus, getBuildLog } from './api.js';
import checkBuildStatus from './check-build-status.js';
import terminal from './terminal.js';
import buildHeader from './build-header.js';

let build;
let reader;
export default async function checkForBuild() {
const status = await checkBuildStatus();

async function finish() {
try {
await reader.cancel();
} finally {
build = null;
}
}
// Prefer the current build, but fallback to the previous one
const build = status.current ?? status.previous;

function processLog({ done, value }) {
if (done) {
return finish();
}
// Render the build header info
buildHeader(build);

if (terminal) {
terminal.write(value);
if (!build) {
return;
}

// Recursively invoke processLog until `done === true`
return reader.read().then(processLog).catch(finish);
}

export default async function checkForBuild() {
const status = await checkBuildStatus();
buildHeader(status);

// If we're already building, skip this check
if (build) {
const reader = await build.getReader();
if (!reader) {
return;
}

if (status.building) {
terminal.clear();
reader = await getBuildLog();
if (reader) {
// eslint-disable-next-line require-atomic-updates
build = { reader, title: status.title, startedAt: status.startedAt };
reader.read().then(processLog).catch(finish);
const finish = async () => {
try {
await reader.cancel();
} catch (err) {
console.warn('Unable to clean up build log reader');
}
}
};

const processLog = () => {
reader
.read()
.then(({ done, value }) => {
if (done) {
return finish();
}
// Write the data to the terminal
terminal.write(value);
// Recursively invoke processLog until `done === true`
return processLog();
})
.catch(finish);
};

humphd marked this conversation as resolved.
Show resolved Hide resolved
// Start processing the log output messages
terminal.clear();
processLog();
}
82 changes: 55 additions & 27 deletions tools/autodeployment/builds.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const { logger, createError } = require('@senecacdot/satellite');
const shell = require('shelljs');
const mergeStream = require('merge-stream');
const streamBuffers = require('stream-buffers');

class Build {
constructor(type, githubData) {
Expand All @@ -11,11 +12,12 @@ class Build {

finish(code) {
logger.debug({ code }, 'build finished');
this.stopDate = new Date();
this.stoppedDate = new Date();
this.code = code;

// Drop the output stream
if (this.out) {
this.out.removeAllListeners();
this.out = null;
}
// Drop the deploy process
Expand All @@ -31,9 +33,10 @@ class Build {
startedDate: this.startedDate,
};

if (this.stopDate) {
build.stopDate = this.stopDate;
if (this.stoppedDate) {
build.stoppedDate = this.stoppedDate;
}

if (this.code) {
build.code = this.code;
}
Expand Down Expand Up @@ -86,6 +89,12 @@ function run() {

// Combine stderr and stdout, like 2>&1
build.out = mergeStream(build.proc.stdout, build.proc.stderr);

// Set up a build log cache
build.cache = new streamBuffers.WritableStreamBuffer();

// As data gets added to the build log, cache it
build.out.on('data', (data) => build.cache.write(data));
}

module.exports.addBuild = function (type, githubData) {
Expand All @@ -101,38 +110,57 @@ module.exports.buildStatusHandler = function handleStatus(req, res) {
res.json(builds);
};

module.exports.buildLogHandler = function handleLog(req, res, next) {
const build = builds.current;
if (!(build && build.out)) {
next(createError(404, 'no build log found'));
return;
module.exports.buildLogHandler = function (buildName) {
if (!(buildName === 'current' || buildName === 'previous')) {
throw new Error(`invalid build name: ${buildName}`);
}

const { out } = build;
return function handleLog(req, res, next) {
const build = builds[buildName];

res.writeHead(200, { 'Content-Type': 'text/plain' });
if (!build) {
next(createError(404, 'no build log found'));
return;
}

let onData;
let onError;
let onEnd;
res.writeHead(200, { 'Content-Type': 'text/plain' });

function end(message) {
if (message) {
res.write(message);
// Send the cached build log, which is either everything, or everything so far
const buildLog = build.cache.getContents();
if (buildLog) {
res.write(buildLog);
}

out.removeListener('data', onData);
out.removeListener('error', onError);
out.removeListener('end', onEnd);
// If we don't have a build happening, we're done
const { out } = build;
if (!out) {
res.end();
return;
}

res.end();
}
// Otherwise stream the build output as it comes in
let onData;
let onError;
let onEnd;

function end(message) {
if (message) {
res.write(message);
}

out.removeListener('data', onData);
out.removeListener('error', onError);
out.removeListener('end', onEnd);

res.end();
}

onData = (data) => res.write(data);
onError = () => end('Error, end of log.');
onEnd = () => end('Build Complete.');
onData = (data) => res.write(data);
onError = () => end('Error, end of log.');
onEnd = () => end('Build Complete.');

out.on('data', onData);
out.on('error', onError);
out.on('end', onEnd);
out.on('data', onData);
out.on('error', onError);
out.on('end', onEnd);
};
};
Loading