Skip to content

Instantly share code, notes, and snippets.

@itssimple
Created January 20, 2020 07:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save itssimple/aa39f2f3379d909217a256c2d890e071 to your computer and use it in GitHub Desktop.
Save itssimple/aa39f2f3379d909217a256c2d890e071 to your computer and use it in GitHub Desktop.
AppVeyor build dashboard (Requires that you log in to AppVeyor in the same browser to get the cookie for the websocket connection)
<?php
include_once 'curl_client.php';
$appVeyorDomain = getenv('APPVEYOR_DOMAIN');
$appVeyorToken = getenv('APPVEYOR_TOKEN');
$appVeyorClient = new cURLClient( "https://$appVeyorDomain", [
"Authorization: Bearer $appVeyorToken"
] );
$projects = $appVeyorClient->GET( "/api/account/AppVeyor/projects", null );
$flattenedList = array();
$currentBuilds = array();
foreach ( $projects as $k => $project ) {
if ( is_numeric( $k ) ) {
$proj = new stdClass;
$proj->projectId = $project['projectId'];
$proj->name = $project['name'];
$flattenedList[] = $proj;
foreach ( $project['builds'] as $i => $_build ) {
$build = new stdClass;
$build->ProjectId = $project['projectId'];
$build->ProjectName = $project['name'];
$build->BuildId = $_build['buildId'];
$build->CommitId = $_build['commitId'];
$build->Status = $_build['status'];
$build->Branch = $_build['branch'];
$build->CommitMessage = $_build['message'];
$build->AuthorUsername = $_build['authorUsername'];
$build->StartTime = $_build['created'];
$build->EndTime = $_build['finished'];
$currentBuilds[] = $build;
}
}
}
$webSocketConnection = $appVeyorClient->POST( '/BrowserHub/negotiate', null );
?>
<?php
if ( ! isset( $_REQUEST['norefresh'] ) ) {
header( "Refresh: 30" );
}
?>
<!DOCTYPE html>
<html>
<head>
<title>Build Dashboard</title>
<style type="text/css">
body {
background-color: #3b3b3b;
padding: 0;
margin: 0;
}
</style>
</head>
<body>
<build-container id="buildcontainer">
<?php foreach ( $currentBuilds as $build ) { ?>
<build-item
id="appveyor-build-<?php echo $build->BuildId; ?>"
state="<?php echo $build->Status; ?>"
start="<?php echo $build->StartTime; ?>"
project="<?php echo $build->ProjectName; ?>"
branch="<?php echo $build->Branch; ?>"
author="<?php echo $build->AuthorUsername; ?>"
commitmessage="<?php echo $build->CommitMessage; ?>"></build-item>
<?php } ?>
</build-container>
<div id="log"></div>
<script type="text/javascript">
class AppVeyorWebSocketClient {
AppVeyorProjects = <?php echo json_encode( $flattenedList ); ?>;
subscriptionItems = [];
#invocationId = 0;
#ws;
get newInvocationId() {
return (this.#invocationId++).toString();
}
constructor() {
this.AppVeyorProjects.forEach(project => {
this.subscriptionItems.push({
arguments: [null, project.projectId],
invocationId: this.newInvocationId,
streamIds: [],
target: "subscribeProjectEvents",
type: 1
})
});
this.#ws = new WebSocket('wss://<?php echo $appVeyorDomain; ?>/BrowserHub?id=<?php echo $webSocketConnection['connectionId']; ?>');
this.#ws.onmessage = (data) => this.handleWs(data);
this.#ws.onopen = () => {
this.sendData({
protocol: 'json',
version: 1
});
};
}
handleWs(data) {
let messages = data.data.replace(/\u0000+$/g, '').split("\x1e");
messages.filter(m => m !== '').forEach((message) => {
switch (message) {
case '{}':
this.sendData(this.subscriptionItems);
break;
case '{"type":6}':
this.sendData({type: 6}, false);
break;
default:
this.parseMessage(message);
break;
}
});
}
parseMessage(message) {
let obj = JSON.parse(message);
if (obj.type) {
if (obj.type === 1 && obj.target) {
switch (obj.target) {
case 'onEvent':
this.handleSignalREvent(obj.arguments[0], obj.arguments[1]);
break;
}
} else {
console.log(`${new Date().toISOString()} <= `, obj);
}
} else {
console.log(`${new Date().toISOString()} <= `, obj);
}
}
handleSignalREvent(eventString, eventArgument) {
console.log(`${new Date().toISOString()} <= `, eventString, eventArgument);
switch (eventString) {
case "project_buildAdded":
this.registerNewJob(eventArgument);
break;
case "project_buildDetailsChanged":
this.handleBuildUpdated(eventArgument);
break;
case "project_buildJobsAdded":
this.handleNewBuildJob(eventArgument);
break;
}
}
registerNewJob(buildInfo) {
let project = this.AppVeyorProjects.find(p => p.projectId === buildInfo.projectId);
if (project) {
let buildData = new AppVeyorBuildData();
buildData.ProjectId = project.projectId;
buildData.ProjectName = project.name;
buildData.BuildId = buildInfo.build.buildId;
buildData.CommitId = buildInfo.build.commitId;
buildData.Status = buildInfo.build.status;
buildData.Branch = buildInfo.build.branch;
buildData.CommitMessage = buildInfo.build.message;
buildData.AuthorUsername = buildInfo.build.authorUsername;
buildData.StartTime = new Date(Date.parse(buildInfo.build.created));
window.buildJobs[buildInfo.build.buildId] = buildData;
buildData.renderItem(document.getElementById('buildcontainer'));
}
}
handleNewBuildJob(buildInfo) {
for (let job of buildInfo.jobs) {
this.sendData({
arguments: [null, job.jobId],
invocationId: this.newInvocationId,
streamIds: [],
target: "subscribeBuildJobEvents",
type: 1
});
}
}
handleBuildUpdated(buildInfo) {
if (window.buildJobs[buildInfo.buildId]) {
let RunningJob = window.buildJobs[buildInfo.buildId];
if (RunningJob) {
if (buildInfo.status !== null) {
window.buildJobs[buildInfo.buildId].Status = buildInfo.status;
}
window.updateBuildItem(buildInfo.buildId);
}
}
}
sendData(obj, echo = true) {
if (echo) {
console.log(`${new Date().toISOString()} => `, obj);
}
let sendMessage = (Array.isArray(obj) ? obj.map(o => JSON.stringify(o)).join("\x1e") : JSON.stringify(obj));
this.#ws.send(sendMessage + "\x1e");
}
}
window.oldBuilds = <?php echo json_encode( $currentBuilds ); ?>;
window.buildJobs = {};
class AppVeyorBuildData {
ProjectId = 0;
BuildId = 0;
JobId = "";
Status = "";
AuthorUsername = "";
Branch = "";
CommitId = "";
CommitMessage = "";
ConsoleMessage = "";
ConsoleMessages = [];
Message = "";
CompilationMessages = [];
TestStatus = {
Total: 0,
Successful: 0,
Failed: 0
};
Artifacts = [];
StartTime = new Date();
EndTime = new Date();
LatestUpdate = new Date();
renderItem(container) {
let item = new BuildItem();
item.setAttribute('id', 'appveyor-build-' + this.BuildId);
item.setAttribute('state', this.Status);
item.setAttribute('branch', this.Branch);
item.setAttribute('project', this.ProjectName);
item.setAttribute('start', this.StartTime.toISOString());
item.setAttribute('update', this.LatestUpdate.toISOString());
item.setAttribute('author', this.AuthorUsername);
item.setAttribute('commitmessage', this.CommitMessage);
container.prepend(item);
}
}
function updateBuildItem(buildId) {
let newInfo = window.buildJobs[buildId];
let item = document.getElementById(`appveyor-build-${buildId}`);
item.setAttribute('state', newInfo.Status);
}
class BuildContainer extends HTMLElement {
constructor() {
super();
let css = document.createElement('style');
css.textContent = `
build-container {
display: grid;
grid-template-columns: 32.5% 32.5% 32.5%;
grid-gap: 1rem;
grid-auto-rows: minmax(5rem, auto);
}
build-item {
font-family: "Segoe UI Light", "Segoe UI", Arial, Helvetica, Verdana, "Sans Serif";
color: #FFF;
font-size: 2rem;
max-width: 100%;
}
build-item[state="queued"] {
border-left: 1rem #3eb4fe solid;
background-color: #27659c;
}
build-item[state="starting"] {
border-left: 1rem #3fddfc solid;
background-color: #377f8c;
}
build-item[state="running"] {
border-left: 1rem #fcfc3f solid;
background-color: #9c9c27;
}
build-item[state="success"] {
border-left: 1rem #3efa3e solid;
background-color: #279c27;
}
build-item[state="failed"] {
border-left: 1rem #fa3e3e solid;
background-color: #9c2727;
}
build-item[state="cancelled"] {
border-left: 1rem #D0D0D0 solid;
background-color: #A1A1A1;
}
build-item .buildTitle {
border-bottom: 0.2rem solid #fff;
font-weight: bold;
padding: 0.5rem;
padding-left: 1rem;
padding-right: 1rem;
}
build-item .buildBody {
padding: 0.5rem;
padding-left: 1rem;
padding-right: 1rem;
}
`.trim();
this.prepend(css);
}
}
class BuildItem extends HTMLElement {
constructor() {
super();
let title = document.createElement('div');
title.className = 'buildTitle';
title.innerHTML = `<span data-field="project" data-position="textContent"></span> - <span data-field="branch" data-position="textContent"></span>`;
this.appendChild(title);
let body = document.createElement('div');
body.className = 'buildBody';
body.innerHTML = '<span data-field="start" data-position="textContent" data-format="date|m-d H:i"></span> - <span data-field="state" data-position="textContent"></span><hr />' +
'<span data-field="commitmessage" data-position="textContent"></span> //<span data-field="author" data-position="textContent"></span>';
this.appendChild(body);
}
attributeChangedCallback(name, oldValue, newValue) {
let updateFields = this.querySelectorAll(`[data-field="${name}"]`);
for (let field of updateFields) {
let targetUpdate = field.getAttribute('data-position');
if (field[targetUpdate] !== undefined) {
let format = field.getAttribute('data-format');
if (format) {
let formatParts = format.split('|');
switch (formatParts[0]) {
case 'date':
let _d = new Date(newValue);
let monthVal = _d.getMonth() + 1;
if (monthVal < 10) {
monthVal = '0' + monthVal;
}
let dayVal = _d.getDate();
if (dayVal < 10) {
dayVal = '0' + dayVal;
}
let hourVal = _d.getHours();
if (hourVal < 10) {
hourVal = '0' + hourVal;
}
let minuteVal = _d.getMinutes();
if (minuteVal < 10) {
minuteVal = '0' + minuteVal;
}
newValue = formatParts[1]
.replace(/m/g, monthVal.toString())
.replace(/d/g, dayVal.toString())
.replace(/H/g, hourVal.toString())
.replace(/i/g, minuteVal.toString());
break;
}
}
field[targetUpdate] = newValue;
}
}
}
static get observedAttributes() {
return ['state', 'console', 'test', 'project', 'branch', 'commitmessage', 'author', 'start'];
}
}
customElements.define('build-container', BuildContainer);
customElements.define('build-item', BuildItem);
for (let build of window.oldBuilds) {
window.buildJobs[build.BuildId] = build;
}
window.appveyorClient = new AppVeyorWebSocketClient();
function removeOldBuilds() {
var allBuildItems = document.querySelectorAll('build-item');
let removeItems = [];
for (let item of allBuildItems) {
if (new Date(item.getAttribute('start')) < new Date().setDate(new Date().getDate() - 5)) {
removeItems.push(item.getAttribute('id'));
}
}
for (let remove of removeItems) {
let i = document.getElementById(remove);
i.parentNode.removeChild(i);
}
}
setInterval(removeOldBuilds, 30000);
removeOldBuilds();
</script>
</body>
</html>
<?php
/**
* Class cURLClient
*/
class cURLClient {
protected $user_agent = 'It\'s Simple cURL Client 0.1';
/**
* @var string
*/
private $host = '';
/**
* @var array
*/
private $default_headers;
/**
* cURLClient constructor.
*
* @param $host string Sets the host
* @param array $default_headers Sets the default headers
*/
public function __construct( $host, $default_headers = array() ) {
$this->host = $host;
$this->default_headers = $default_headers;
}
/**
* @param resource $curl
*
* @return mixed
*/
private function execute_request( $curl ) {
$headers = array();
curl_setopt( $curl, CURLOPT_HEADERFUNCTION, function ( $curl, $header ) use ( &$headers ) {
$len = strlen( $header );
$header = explode( ':', $header, 2 );
if ( count( $header ) < 2 ) // ignore invalid headers
{
return $len;
}
$headers[ strtolower( trim( $header[0] ) ) ][] = trim( $header[1] );
return $len;
} );
$r = curl_exec( $curl );
$i = curl_getinfo( $curl );
$obj = array();
if ( false === $r || ( json_decode( $r ) && isset( json_decode( $r )->error ) ) || ( $i['http_code'] < 200 || $i['http_code'] > 299 ) ) {
curl_close( $curl );
if ( null !== json_decode( $r ) ) {
$obj = json_decode( $r, true );
} else {
$obj['data'] = $r;
}
$obj['@curl'] = $i;
$obj['@headers'] = $headers;
$obj['@error'] = $r;
return $obj;
}
curl_close( $curl );
if ( ( substr( $r, 0, 1 ) === '{' || substr( $r, 0, 1 ) === '[' )
&& null !== json_decode( $r, true ) ) {
$obj = json_decode( $r, true );
} else {
if ( substr( $r, 0, 1 ) === '"' ) {
$obj['data'] = json_decode( $r, true );
} else {
$obj['data'] = $r;
}
}
$obj['@curl'] = $i;
$obj['@headers'] = $headers;
return $obj;
}
/**
* @param $endpoint string Where are we going with this request?
* @param $params string|object|array Contains all parameters that we want to pass to the API
* @param bool $is_json Decides if this is a post with JSON
*
* @return mixed
*/
public function POST( $endpoint, $params, $is_json = true ) {
return $this->make_request( 'POST', $endpoint, $params, $is_json );
}
/**
* @param $endpoint string Where are we going with this request?
* @param $params string|object|array Contains all parameters that we want to pass to the API
* @param bool $is_json Decides if this is a post with JSON
*
* @return mixed
*/
public function PUT( $endpoint, $params, $is_json = true ) {
return $this->make_request( 'PUT', $endpoint, $params, $is_json );
}
/**
* @param $endpoint string Where are we going with this request?
* @param $params string|object|array Contains all parameters that we want to pass to the API
* @param bool $is_json Decides if this is a post with JSON
*
* @return mixed
*/
public function DELETE( $endpoint, $params, $is_json = true ) {
return $this->make_request( 'DELETE', $endpoint, $params, $is_json );
}
/**
* @param $endpoint string Where are we going with this request?
* @param $params string|object|array Contains all parameters that we want to pass to the API
* @param bool $is_json Decides if this is a post with JSON
*
* @return mixed
*/
public function PATCH( $endpoint, $params, $is_json = true ) {
return $this->make_request( 'PATCH', $endpoint, $params, $is_json );
}
/**
* @param string $type
* @param string $endpoint
* @param string|array|object $params
* @param string $method_name
* @param bool $is_json
*
* @return mixed
*/
private function make_request( $type, $endpoint, $params, $is_json = true ) {
$c = $this->get_curl_object( $endpoint );
$headers = $this->default_headers;
$data = null;
if ( $is_json ) {
$headers[] = 'Content-Type: application/json';
$data = json_encode( $params );
$headers[] = 'Content-Length: ' . strlen( $data );
} else {
$data = http_build_query( $params );
}
$this->set_headers( $c, $headers );
curl_setopt( $c, CURLOPT_CUSTOMREQUEST, $type );
curl_setopt( $c, CURLOPT_POSTFIELDS, $data );
$result = $this->execute_request( $c );
return $result;
}
/**
* @param string $endpoint
* @param object|array $params
* @param string $method_name
*
* @return mixed
*/
public function GET( $endpoint, $params = null ) {
$c = $this->get_curl_object( $endpoint . ( $params != null ? '?' . http_build_query( $params ) : '' ) );
$headers = $this->default_headers;
$this->set_headers( $c, $headers );
$result = $this->execute_request( $c );
return $result;
}
/**
* @param string $endpoint
*
* @return resource
*/
private function get_curl_object( $endpoint ) {
if ( ! strpos( $endpoint, '/' ) === 0 ) {
$endpoint = '/' . $endpoint;
}
$c = curl_init( $this->host . $endpoint );
curl_setopt( $c, CURLOPT_RETURNTRANSFER, true );
return $c;
}
/**
* @param resource $curl_object
* @param array $array
*/
private function set_headers( $curl_object, array $array = array() ) {
$std_headers = array();
if ( ! empty( $array ) ) {
$std_headers = array_merge( $std_headers, $array );
}
curl_setopt( $curl_object, CURLOPT_HTTPHEADER, $std_headers );
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment