Skip to content
Snippets Groups Projects
Commit f68befad authored by Austin Anderson's avatar Austin Anderson
Browse files

Create first draft of dashboard generation

parent be3b4c0e
No related branches found
No related tags found
No related merge requests found
# Copyright 2023 The TensorFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ============================================================================
name: Dashboard Generator
on:
# Only the default branch is supported.
branch_protection_rule:
schedule:
- cron: '*/5 * * * *'
# Declare default permissions as read only.
permissions: read-all
jobs:
dashboard:
if: github.repository == 'tensorflow/build' # Don't do this in forks
name: Generate Dashboard
runs-on: ubuntu-latest
permissions:
# Needed to upload the results to code-scanning dashboard.
security-events: write
# Used to receive a badge.
id-token: write
steps:
- name: "Checkout code"
uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3.2.0
with:
persist-credentials: false
- name: Download artifact
id: download-artifact
uses: dawidd6/action-download-artifact@v2
with:
workflow_conclusion: success
- name: Run Script
id: run-script
run:
- cd tf_oss_dashboard
- ./script.sh
- name: "Upload merged data"
uses: actions/upload-artifact@83fd05a356d7e2593de66fc9913b3002723633cb # v3.1.1
with:
name: Merged Data
path: old.json
retention-days: 5
- name: "Upload dashboard"
uses: actions/upload-artifact@83fd05a356d7e2593de66fc9913b3002723633cb # v3.1.1
with:
name: Dashboard
path: dashboard.html
retention-days: 5
......@@ -67,6 +67,8 @@ Want to add your own project to this list? It's easy: check out
### WIP / Other
* [**Directory Template**](directory_template): Example short description.
* [**TF OSS Dashboard**](tf_oss_dashboard): Dashboard for all continuous
statuses on TF GitHub Commits.
* [**Tekton CI**](tekton): perfinion's experimental directory for using Tekton
CI with TensorFlow
......
# TF OSS Dashboard
Dashboard for all continuous statuses on TF GitHub Commits.
Maintainer: @angerson (TensorFlow, SIG Build)
categories:
TF DevInfra:
- Py+CPP Test Suite - Ubuntu GPU, Python 3.9
- Py+CPP Test Suite - Ubuntu CPU, Python 3.9
- Code Check - Full
- MacOS CPU Python3.9 Shard 1/2
- MacOS CPU Python3.9 Shard 2/2
Everything Else:
- Default
Ignorable:
- "? / cla/google"
- import/copybara
- kokoro
#!/usr/bin/env python3
from collections import defaultdict
from jinja2 import Environment, FileSystemLoader
import arrow
import itertools
import json
import markdown
import pypugjs
import re
import sys
import yaml
with open('config.yaml', 'r') as f:
yaml_config = yaml.safe_load(f)
category_map = {}
for name, items in yaml_config["categories"].items():
for item in items:
category_map[item] = name
data = json.load(sys.stdin)
records = []
cl_re = re.compile(r"PiperOrigin-RevId: (\d+)")
for d in data["data"]["repository"]["defaultBranchRef"]["target"]["history"]["nodes"]:
record = {
"commit": d["oid"],
"commit_id": "#" + d["oid"],
"date": arrow.get(d["committedDate"], "YYYY-MM-DDTHH:mm:ssZ"),
"commit_url": d["commitUrl"],
"commit_summary": d["messageHeadline"],
"commit_body": d["messageBody"].replace("\n", "</br>"),
"commit_message": d["message"],
"short_commit": d["oid"][0:7]
}
record["date_human"] = record["date"].to('US/Pacific').format("ddd, MMM D [at] h:mma ZZZ")
has_cl = cl_re.search(d["message"])
if has_cl:
record["cl"] = has_cl.group(1)
record["cl_url"] = f"http://cl/{record['cl']}"
for item in d["statusCheckRollup"]["contexts"]["nodes"]:
if "context" in item:
sub = {
"name": item["context"],
"type": "status check",
"state": item["state"],
"result_url": item["targetUrl"]
}
else:
default = { "workflow": { "name": "?" } }
sub = {
"name": (item["checkSuite"]["workflowRun"] or default)["workflow"]["name"] + " / " + item["name"],
"type": "github action",
"state": item["conclusion"] or item["status"],
"result_url": item["url"]
}
raw = record | sub
raw["category"] = category_map.get(raw["name"], "Everything Else")
raw["raw"] = json.dumps(raw, default=str)
records.append(raw)
records.sort(key=lambda l: l["date"], reverse=True)
by_name = defaultdict(list)
for record in records:
by_name[record["name"]].append(record)
for name, jobs in by_name.items():
new_jobs = []
new_jobs.append({"date_tag": jobs[0]["date"].strftime("%a %b %d")})
first_non_pending_status = None
for job in jobs:
if first_non_pending_status in [None, "PENDING", "IN_PROGRESS", "QUEUED"]:
first_non_pending_status = job["state"]
for left, right in itertools.pairwise(jobs):
left["previous_diff_url"] = f"https://github.com/tensorflow/tensorflow/compare/{left['commit']}..{right['commit']}"
new_jobs.append(left)
if left["date"].date() != right["date"].date():
new_jobs.append({"date_tag": right["date"].strftime("%a %b %d")})
new_jobs.append(jobs[-1])
new_jobs[0]["first_non_pending_status"] = first_non_pending_status
new_jobs[0]["is_pending"] = new_jobs[1]["state"] in ["PENDING", "IN_PROGRESS", "QUEUED"]
new_jobs[0]["card_class"] = " ".join([first_non_pending_status, "CARD_PENDING" if new_jobs[0]["is_pending"] else "CARD_NOT_PENDING"])
by_name[name] = new_jobs
by_group = defaultdict(dict)
for category, items in yaml_config["categories"].items():
by_group[category] = {}
for name, jobs in sorted(by_name.items(), key=lambda x: x[0]):
by_group[category_map.get(name, "Everything Else")][name] = jobs
by_commit = defaultdict(list)
for name, jobs in by_name.items():
for job in jobs:
if "commit" not in job:
continue
by_commit[job["commit"]].append(job)
for name, jobs in by_commit.items():
jobs.sort(key=lambda k: k["name"])
with open("style.css", "r") as f:
css = f.read()
with open("script.js", "r") as f:
js = f.read()
with open("help.md", "r") as f:
helptext = markdown.markdown(f.read())
env = Environment(
loader=FileSystemLoader('.'),
extensions=['pypugjs.ext.jinja.PyPugJSExtension']
)
template = env.get_template('template.html.pug')
now = arrow.now().to('US/Pacific').format("ddd, MMM D [at] h:mma ZZZ")
print(template.render(records=by_name, by_group=by_group, by_commit=by_commit, css=css, js=js, helptext=helptext, now=now))
This dashboard is a work-in-progress.
- Click on a star to favorite that job, placing it at the top of the list.
- Jobs with a diamond in the top-right corner are the jobs that TF DevInfra considers to be "important," and will always appear first, after your favorites.
- Click on a status dot to see the full status for that commit.
- Jobs only appear on this list if they have a result for the last 100 commits.
- Kokoro does not report in-progress jobs, so there are no glowing progress dots.
- Each "diff" links to the diff between this commit and the previous commit for that job. GitHub occasionally does not show a diff here. Take a screenshot and let us know if that happens.
- The "kokoro" job is actually multiple jobs that are all called "kokoro." If you own one of these, you need to change the `github_status_context` github_scm config value to a real name.
#!/usr/bin/env python3
import json
import sys
with open(sys.argv[1], "r") as f:
old = json.load(f)
with open(sys.argv[2], "r") as f:
new = json.load(f)
overlap = False
commits = {}
for commit in old["data"]["repository"]["defaultBranchRef"]["target"]["history"]["nodes"]:
commits[commit["oid"]] = commit
for commit in new["data"]["repository"]["defaultBranchRef"]["target"]["history"]["nodes"]:
if commit["oid"] in commits:
overlap = True
commits[commit["oid"]] = commit
if overlap:
new["data"]["repository"]["defaultBranchRef"]["target"]["history"]["nodes"] = list(commits.values())[0:300]
print(json.dumps(new))
{
repository(name: "tensorflow", owner: "tensorflow") {
defaultBranchRef {
target {
... on Commit {
history(first: 100) {
nodes {
messageHeadline
message
messageBody
commitUrl
statusCheckRollup {
contexts(first: 20) {
totalCount
nodes {
... on StatusContext {
id
state
context
targetUrl
description
}
... on CheckRun {
id
name
detailsUrl
url
status
conclusion
checkSuite {
workflowRun {
workflow {
name
}
}
}
}
}
}
}
oid
committedDate
}
}
}
}
}
}
rateLimit {
limit
cost
remaining
resetAt
}
}
#!/usr/bin/env bash
curl -s https://api.github.com/graphql -X POST \
-H "Authorization: Bearer $GITHUB_TOKEN" \
-H "Content-Type: application/json" \
-d "$(jq -c -n --arg query "$(cat query.graphql)" '{"query":$query}')"
pyyaml
arrow
markdown
pypugjs
jinja2
function reorder() {
let favorites = JSON.parse(localStorage.getItem('favorites') || '{}')
$('.important').each(function() {
$(this).parent().parent().prepend($(this).parent())
})
$('.favorite').each(function() {
let current = $(this).parent().attr("data-name")
if (favorites[current]) {
$(this).parent().parent().prepend($(this).parent())
}
})
}
$('.favorite').on('click', function() {
let current = $(this).parent().attr("data-name")
let favorites = JSON.parse(localStorage.getItem('favorites') || '{}')
if (favorites[current] !== null) {
favorites[current] = !favorites[current]
} else {
favorites[current] = true
}
$(this).text(favorites[current] ? '' : '')
localStorage.setItem('favorites', JSON.stringify(favorites))
reorder()
})
let favorites = JSON.parse(localStorage.getItem('favorites') || '{}')
$('.favorite').each(function() {
let current = $(this).parent().attr("data-name")
$(this).text(favorites[current] ? '' : '')
})
reorder()
#!/bin/bash
set -euxo pipefail
pip install -r requirements.txt
./query_api.sh > new.json
./merge.py old.json new.json > merged.json
cat merged.json ./dashboard.py > dashboard.html
mv merged.json old.json
body {
background-color: #113;
}
.navbar {
background-color: #002 !important;
}
.tf-date {
line-height: 0;
background-color: darkgoldenrod;
}
.tf-fulldate {
background-color: darkgoldenrod;
}
.tf-cl {
background-color: green;
}
.tf-nocl {
background-color: darkslategray;
}
.favorite, .important {
opacity: 60%;
}
.tf-hash {
background-color: darkolivegreen;
}
.tf-diff {
background-color: saddlebrown;
}
.tf-nodiff {
background-color: darkslategray;
}
.card {
flex-basis: 20em;
background-image: linear-gradient(rgb(0 0 0/30%) 0 0);
}
.CARD_PENDING {
background-image:
repeating-linear-gradient(
-45deg,
#0006,
#0006 .5rem,
#0004 .5rem,
#0004 1rem
);
background-size: 200% 200%;
animation: barberpole 100s linear infinite;
}
@keyframes barberpole {
100% {
background-position: 100% 100%;
}
}
/* See https://www.w3schools.com/cssref/css_colors.php */
/* See https://htmlcolorcodes.com/color-names/ */
.ERROR, .FAILURE, .TIMED_OUT {
background-color: firebrick;
}
.EXPECTED, .PENDING, .QUEUED, .IN_PROGRESS {
animation: pending 2s linear infinite;
}
@keyframes pending {
0% {
background-color: lightskyblue;
}
50% {
background-color: dodgerblue;
}
100% {
background-color: lightskyblue;
}
}
.SUCCESS {
background-color: green;
}
.CANCELED {
background-color: black;
}
.NEUTRAL {
background-color: slategray;
}
!!! 5
html(lang="en")
head
title TensorFlow GitHub Status
meta(charset="UTF-8")
link(href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" crossorigin="anonymous")
script(src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js" integrity="sha384-w76AqPfDkMBDXo30jS1Sgez6pr3x5MlQ1ZAGC+nuZB+EYdgRZgiwxhTBTkF7CXvN" crossorigin="anonymous")
script(src="https://code.jquery.com/jquery-3.6.4.slim.min.js" integrity="sha256-a2yjHM4jnF9f54xUQakjZGaqYs/V1CYvWpoqZzC2/Bw=" crossorigin="anonymous")
style= css
body
nav.navbar.navbar-dark.navbar-expand-lg.py-0.mb-2
.container-fluid.py-0
.d-flex.flex-row
a.navbar-brand TensorFlow GitHub CI
.navbar-text Last updated on #{now}
ul.navbar-nav
li.nav-item
a.nav-link(role="button" data-bs-toggle="modal" data-bs-target="#help-modal") Help
.d-flex.flex-column.gap-1.m-1
each category, everythin in by_group.items()
.fw-bold.ps-4
span.text-light= category
.d-flex.flex-row.flex-wrap.gap-1.m-1
each name, tests in everythin.items()
.card.flex-shrink-1.flex-grow-1(class=tests[0]["card_class"] data-name=name)
span.position-absolute.lh-1.fs-2.favorite ☆
if tests[1]["is_important"]
span.position-absolute.end-0.lh-1.fs-2.important.pe-1 ♦
.card-body
h4.text-center.fw-bold= name
.d-flex.flex-row.flex-wrap.gap-1
each test in tests
if "date_tag" in test:
.btn.p-2.tf-date.cursor-pointer= test["date_tag"]
else
.btn.p-2(class=test["state"] data-record=test.raw data-bs-toggle='modal' data-bs-target=test["commit_id"])
each commit, jobs in by_commit.items()
.modal.modal-lg.fade(tabindex='-1' aria-labelledby=commit id=commit aria-hidden='true')
.modal-dialog
.modal-content
.modal-header
.d-flex.flex-column
div.fw-bold.text-wrap
a.badge.me-1.tf-hash(href=jobs[0]["commit_url"])= jobs[0]["short_commit"]
=jobs[0]["commit_summary"]
.modal-body
.d-flex.flex-row
a.badge.me-1.tf-fulldate(href=jobs[0]["commit_url"])= jobs[0]["date_human"]
if "cl" in jobs[0]:
a.badge.tf-cl(href=jobs[0]["cl_url"]) cl/#{jobs[0]["cl"]}
else
span.badge.tf-nocl No CL Attached
.d-flex.flex-column.text-wrap
!{jobs[0]["commit_body"]}
hr
each job in jobs
.d-flex.flex-row.gap-1.py-1
a.badge.my-auto(href=job["result_url"] class=job["state"])= job["state"]
if "previous_diff_url" in job:
a.badge.my-auto.text-center.tf-diff(href=job["previous_diff_url"]) Diff
else
span.badge.my-auto.tf-nodiff No Diff
span.lh-1= job["name"]
.modal.modal-lg.fade(tabindex='-1' aria-labelledby="help-modal" id="help-modal" aria-hidden='true')
.modal-dialog
.modal-content
.modal-header.fw-bold Dashboard Help
.modal-body !{helptext}
script !{js}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment