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

  1. Basic knowledge of Javascript
  2. Node.js installed on your system (version >= 10.3)
  3. A CircleCI account

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.

Add Token - CircleCI

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:

  1. express creates the application server
  2. axios makes HTTP requests to the CircleCI API
  3. cors handles CORS issues
  4. dotenv stores the API token in an environment variable
  5. body-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 a project_slug to retrieve the pipelines that have been triggered on a project. A project_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 a project_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}} &nbsp;-&nbsp;
                                    <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.

Projects Load - Dashboard

Click a project to load its pipelines.

Pipelines Load - Dashboard

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!


Fikayo is a fullstack developer and author with over a decade of experience developing web and mobile solutions. He is currently the Software Lead at Tech Specialist Consulting and develops courses for Packt and Udemy. He has a strong passion for teaching and hopes to become a full-time author.