Using the CircleCI API to build a deployment summary dashboard
Fullstack Developer and Tech Author
The CircleCI API provides a gateway for developers to retrieve detailed information about their pipelines, projects, and workflows, including which users are triggering the pipelines. This gives developers great control over their CI/CD process by supplying endpoints that can be called to fetch information and trigger processes remotely from the user’s applications or automation systems. In this tutorial, you will learn and practise how to use the API to create a simple personalized dashboard to monitor your deployment pipelines.
Prerequisites
With all these installed and set up, it is time to begin the tutorial.
Getting a CircleCI API token
Your account needs full read and write permissions to make authenticated calls to the API. To grant those permissions, you will need to create a Personal API token. Go to your CircleCI User settings, then click Personal API Tokens. Click the Create New Token button. In the Token name field, enter a name that you are likely to remember, and click the Add API Token button.
The token will be displayed for you to copy to a safe location. Make sure you copy it now, because it will not be shown again.
Setting up the Insights dashboard project
The dashboard we create in this tutorial will be a Node.js application with a few endpoints for making calls to the CircleCI API. The application will return the dashboard page at its base (/
) route.
To begin, create a new project and navigate to the root of the folder:
mkdir insights-dashboard
cd insights-dashboard
Next, scaffold a basic package.json
file:
npm init -y
Five packages will need to be installed:
express
creates the application serveraxios
makesHTTP
requests to the CircleCI APIcors
handles CORS issuesdotenv
stores the API token in an environment variablebody-parser
parses request data in JSON format
Install these all at once using this command:
npm install express axios cors dotenv body-parser
Next, create a .env
file to store the API token:
API_KEY=YOUR_API_TOKEN
Replace YOUR_API_TOKEN
with the Personal API token you copied earlier.
Creating the dashboard endpoints
As I mentioned earlier, the root of the application will return the dashboard page, which is an index.html
file, using the base (/
) endpoint. The project application also uses other endpoints. At the root of the project, create a new file server.js
and enter this code:
require("dotenv").config();
const express = require("express");
const path = require("path");
const app = express();
const cors = require('cors');
let bodyParser = require("body-parser");
const axios = require("axios");
app.use(cors());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
let port = process.env.PORT || "5000";
const api_v1 = "https://circleci.com/api/v1.1/";
const api_v2 = "https://circleci.com/api/v2/";
axios.defaults.headers.common['Circle-Token'] = process.env.API_KEY;
app.get("/", (req, res) => {
res.sendFile(path.join(__dirname+'/index.html'));
});
app.get("/getprojects", async (req, res) => {
let projects = await axios.get(`${api_v1}projects`);
res.send(projects.data);
});
app.get("/getpipelines", async (req, res) => {
const project_slug = req.query.project_slug;
let pipelines = await axios.get(`${api_v2}project/${project_slug}/pipeline`);
res.send(pipelines.data);
})
app.post("/triggerpipeline", async (req, res) => {
const project_slug = req.body.project_slug;
try {
const trigger = await axios.post(`${api_v2}project/${project_slug}/pipeline`);
res.send(trigger.data);
} catch (error) {
res.send(error)
}
})
app.get("/getworkflows/:pipeline_id", async (req, res) => {
const pipeline_id = req.params.pipeline_id;
let workflows = await axios.get(`${api_v2}pipeline/${pipeline_id}/workflow`);
res.send(workflows.data);
})
app.listen(port, () => {
console.log(`App Running at http://localhost:${port}`);
})
Let me take a moment to break down what is happening in this file. First, required packages are imported and middleware is setup for cors
and body-parser
. Next, api_v1
and api_v2
are defined to hold the URLs for the version 1 and version 2 of the CircleCI API respectively.
Then, the axios
module is configured to send the API token in each request made by setting the Circle-Token
header to the token stored in the environment file (.env
).
The base (/
) endpoint is setup next to return the index.html
that will contain the dashboard code. This file will be created in the next section.
Other endpoints are then defined as follows:
-
/getprojects
uses the CircleCI API version 1 to retrieve the list of projects from your account. Retrieving projects is not available for version 2 of the API, but it is already in the works. -
/getpipelines
calls the v2 API with aproject_slug
to retrieve the pipelines that have been triggered on a project. Aproject_slug
is a “triplet” identifier format for a CircleCI project. It is of the form<project_type>/<org_name>/<repo_name>
. You can read this article for more information. -
/triggerpipeline
calls the v2 API with aproject_slug
to trigger a new pipeline to run on a project.
Finally, the application is programmed to listen on the specified port.
To complete this section of the tutorial, open package.json
and add a start
script:
"scripts" : {
.....,
"start": "node server.js"
}
Creating the dashboard page
Time to create the dashboard itself. The application will have two columns, one for the projects, which will load immediately. The other column will load the pipelines. There will also be a Trigger Pipeline button to run a new pipeline on a selected project.
To build out the dashboard UI and functionality, we will be using Bootstrap for styling and Vue.js as the frontend framework. We will also be using axios on the front end to make API calls to the endpoints in our application. No reason to fret if you do not understand Vue.js. You can achieve the same functionality using any other framework you prefer, or even vanilla Javascript.
At the root of the project, create the index.html
file and enter:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css" integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.21.0/axios.min.js" integrity="sha512-DZqqY3PiOvTP9HkjIWgjO6ouCbq+dxqWoJZ/Q+zPYNHmlnI2dQnbJ5bxAHpAMw+LXRm4D72EIRXzvcHQtE8/VQ==" crossorigin="anonymous"></script>
<title>Insights Dashboard</title>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<a class="navbar-brand" href="/#">My CircleCI Pipelines Dashboard</a>
<button
class="navbar-toggler"
type="button"
data-toggle="collapse"
data-target="#navbarText"
aria-controls="navbarText"
aria-expanded="false"
aria-label="Toggle navigation">
<span class="navbar-toggler-icon" />
</button>
</nav>
<div id="app">
<div class="container">
<div class="row">
<div class="col-md-6" id="projects-section">
<p v-if="loadingProjects">
<i>Loading Projects...</i>
</p>
<ul v-else id="list" class="list-group">
<li class="list-group-item" v-for="project in projects" v-bind:class="{ active: selectedProject.reponame == project.reponame }" @click="loadPipelines(project)">
{{project.reponame}}
</li>
</ul>
</div>
<div class="col-md-6">
<div v-if="selectedProject.reponame">
<h2>Pipelines <span class="text-primary">[{{selectedProject.reponame}}]</span></h2>
<p>
<button @click="triggerPipeline()" type="button" class="btn btn-success" :disabled="triggeringProjectPipeline">
{{triggeringProjectPipeline ? "Procesing" : "Trigger Pipeline"}}
</button>
</p>
<p v-if="loadingPipelines">
<i>Loading Pipelines</i>
</p>
<div v-else class="list-group" id="pipelines-section">
<a v-for="pipeline in pipelines" href="#" class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">
{{pipeline.number}} -
<span v-if="pipeline.state == 'errored'" class="text-danger">Failed</span>
<span v-else class="text-success">Passed</span>
</h5>
<small>{{pipeline.created_at.substring(0, 10)}}</small>
</div>
<p class="mb-1" v-if="pipeline.trigger">
Trigger Type: <span class="text-danger">{{pipeline.trigger.type}}</span> <br />
Recieved At: <span class="text-sucess">{{pipeline.trigger.received_at.substring(0, 10)}}</span> <br />
Triggered By: <span class="text-primary">{{pipeline.trigger.actor.login}}</span></p>
<small>ID: {{pipeline.id}}</small>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
loadingProjects : false,
projects: [],
selectedProject : {},
loadingPipelines : false,
pipelines : [],
triggeringProjectPipeline : false
},
async created(){
this.loadingProjects = true;
let projects = await axios.get(`getprojects`);
this.projects = projects.data;
this.loadingProjects = false;
},
methods : {
loadPipelines : async function (project) {
this.selectedProject = project;
this.loadingPipelines = true;
const project_slug = `${project.vcs_type}/${project.username}/${project.reponame}`;
let pipelines = await axios.get(`getpipelines?project_slug=${project_slug}`);
console.log(pipelines);
this.pipelines = pipelines.data.items;
this.loadingPipelines = false;
},
triggerPipeline : async function () {
this.triggeringProjectPipeline = true;
let project = this.selectedProject;
const project_slug = `${project.vcs_type}/${project.username}/${project.reponame}`;
let trigger = await axios.post(`triggerpipeline`, {
project_slug
});
console.log(trigger);
this.loadPipelines(project);
this.triggeringProjectPipeline = false;
}
}
})
</script>
<style>
#app {
margin-top: 50px;
}
#projects-section, #pipelines-section{
height: 600px;
overflow: scroll;
}
</style>
</body>
</html>
In the preceding code, the libraries for Bootstrap CSS, Vue.js, and axios
are loaded from their respective CDNs. In the body of the page, two columns are created with Bootstrap’s grid system to format the Projects section, another section for Pipelines, and the Trigger Pipeline button.
Within the script tag, a new Vue.js application instance is created, with data variables to hold the projects
, pipelines
, and selectedProject
. Variables for loadingProjects
, loadingPipelines
, and triggeringProjectPipeline
are also created to manage application state when an API request is made to the /getprojects
, /getpipelines
, and /triggerpipeline
endpoints respectively.
A Vue.js created
lifecycle hook is used to load projects from your account once the dashboard loads. Methods are also defined to load pipelines and to trigger a new pipeline to run in the methods
property of the Vue.js instance.
Lastly, in the <style>
section, basic styling is added to the page.
Testing the dashboard project
It is go time! At the root of the project, run:
npm start
The application should boot up at http://localhost:5000
. Load this address in your browser, and you should see your projects after the brief loading message.
Click a project to load its pipelines.
When a project is clicked, the pipelines for that project are loaded in the next column. Each pipeline entry consists of details like the pipeline status (Passed or Failed), the user/process that triggered the pipeline, the date the pipeline was triggered. and more.
You can also click the Trigger Pipeline button to run a new pipeline. The pipelines list will be refreshed to show the recently triggered pipeline.
Conclusion
As we demonstrated in this tutorial, the CircleCI API gives DevOps professionals the ability to get useful information and create personalized experiences for our projects and pipelines. With just a few lines of code, we have been able to develop an interactive dashboard for our project. Imagine what you can do with the rest of the endpoints available in the CircleCI API.
When it comes to developer team success, finding the right DevOps metrics to measure is crucial. Learn how to measure DevOps success with four key benchmarks for your engineering teams in the 2020 State of Software Delivery: Data-Backed Benchmarks for Engineering Teams.
Happy coding!