Gitlab Pipeline Metadata (via cimd)

Extend gitlab's pipeline list view with a metadata column to show pipeline metadata.

// ==UserScript==
// @name         Gitlab Pipeline Metadata (via cimd)
// @description  Extend gitlab's pipeline list view with a metadata column to show pipeline metadata.
// @version      v0.9.0
// @author       Sebastian Tramp
// @license      apache-2.0
// @homepage     https://github.com/seebi/cimd
// @namespace    https://sebastian.tramp.name/
// @match        https://*/*/-/pipelines*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=gitlab.com
// @require http://code.jquery.com/jquery-latest.js
// ==/UserScript==
/* eslint-env jquery */

var pipeline_data = {}

function getPipelineIDs() {
    /**
     * Get all pipeline IDs from the page
     *
     * @return {String[]} List of Pipeline IDs
     */
    var ids = new Array()
    $("tr[data-testid='pipeline-table-row']").find("a[data-testid='pipeline-url-link']").each(
        function(index){
            ids.push($(this).text().replace("#", ""))
        }
    );
    return ids
}


function getBasePipelineUrl() {
    /**
     * Get base pipeline URL (extracted from window.location.href)
     *
     * Example location: https://gitlab.com/seebi/gitlab-pipeline-metadata/-/pipelines?page=1&scope=all&ref=main
     * -> returns: https://gitlab.com/seebi/gitlab-pipeline-metadata/-/pipelines
     *
     * Example Output: https://gitlab.com/seebi/gitlab-pipeline-metadata/-/jobs/7445767203/artifacts/raw/__metadata__.json
     *
     * @return {String} Base pipeline URL
     */
    var base_regex = /(.+)\/-\/pipelines.*/g;
    var url = base_regex.exec(window.location.href)[1] + "/-/pipelines";
    return url
}

function getJobIDfromPath(path) {
    /**
     * Extract the Job ID from base relative gitlab path.
     *
     * Example path: /seebi/gitlab-pipeline-metadata/-/jobs/7445767203/artifacts/download?file_type=archive
     *
     * @param {String} path Base relative path string
     *
     * @return None
     */
    var job_regex = /jobs\/(.+)\/artifacts/g;
    return job_regex.exec(path)[1];
}

function getArtifactUrl(job_id) {
    /**
     * Get raw JSON URL of __metadata__.json for a job.
     *
     * Example Output: https://gitlab.com/seebi/gitlab-pipeline-metadata/-/jobs/7445767203/artifacts/raw/__metadata__.json
     *
     * @param {String} job_id Gitlab Job ID
     *
     * @return {String} RAW Artifact URL
     */
    return getBasePipelineUrl().replace("pipelines", "jobs") + "/" + job_id + "/artifacts/raw/__metadata__.json"
}

function getDownloadableArtifactUrl(pipeline_id){
    /**
     * Get API endpoint URL for fetching all downloadable artifacts of a pipeline.
     *
     * Example Output: https://gitlab.com/seebi/gitlab-pipeline-metadata/-/pipelines/1391065437/downloadable_artifacts.json
     *
     * @param {String} pipeline_id Gitlab Pipeline ID
     *
     * @return {String} Artifacts URL
     */
    var url = getBasePipelineUrl() + "/" + pipeline_id + "/downloadable_artifacts.json"
    return url
}

function extendTableIfNeeded() {
    /**
     * Extend the CI Table with an additional Metadata column (if needed)
     *
     * @return None
     */
    if ($("div.meta-data-container").length){
        return
    }
    var table = $("div.ci-table")
    if ( table.length ) {
        $("*[data-testid='stages-th']").before('<th role="columnheader" scope="col" aria-colindex="5" data-testid="data-th" class=""><div>Metadata</div></th>')
        $("*[data-label='Stages']").before('<td aria-colindex="5" data-label="Metadata" role="cell" class="pl-p-5!"><div class="meta-data-container">-</div></td>')
    }
}

function createDataWidget(key, item){
    /**
     * Create a single meta data item html snippet <div>.
     *
     * mandatory keys in item object: value
     * optional keys in item object: label, description, image, link and comment
     *
     * @link https://gitlab.com/seebi/gitlab-pipeline-metadata/-/blob/main/README.md#data-__metadata__json
     *
     * @param {String} key Meta data item identifier.
     * @param {Object} item Meta data item object.
     *
     * @return {String} Widget HTML Snippet
     */
    var label = ((item.label) ? item.label + ": " + item.value : key + ": " + item.value)

    var tooltip = "key: " + key + "\nvalue: " + item.value
    tooltip += ((item.label) ? "\nlabel: " + item.label : "")
    tooltip += ((item.description) ? "\ndescription: " + item.description : "")
    tooltip += ((item.image) ? "\nimage: " + item.image : "")
    tooltip += ((item.link) ? "\nlink: " + item.link : "")
    tooltip += ((item.comment) ? "\ncomment: " + item.comment : "")

    var output = '<div data-testid="metadata-' + key + '" class="meta-data-item" title="' + tooltip + '">'
    output += ((item.link) ? "<a href='" + item.link + "'>" : "")
    output += ((item.image) ? '<img src="' + item.image + '" />' : label)
    output += ((item.link) ? "</a>" : "")
    output += "</div"
    return output
}

function extendRow(pipeline_id){
    /**
     * Uses the global pipeline_data variable to append one widget for each metadata item.
     *
     * @param {String} pipeline_id Pipeline ID (scraped from pipeline-url-link)
     *
     * @return None
     */
    extendTableIfNeeded()
    var pipeline_link = $("a[data-testid='pipeline-url-link']:contains('#" + pipeline_id + "')")
    var pipeline_row = pipeline_link.closest("tr[data-testid='pipeline-table-row']")
    var container = pipeline_row.find("div.meta-data-container")
    const items = pipeline_data[pipeline_id].items
    const keys = Object.keys(items)
    container.text("")
    for (const key in keys) {
        var id = keys[key]
        container.append(createDataWidget(id, items[id]))
    }
}

function getDataForPipeline(pipeline_id){
    /**
     * Fetch available __metadata.json__ and collect it in a global pipeline_data object.
     * Optionally start extendRow for a pipeline if __metadata__ is available.
     *
     * @param {String} pipeline_id Pipeline ID (scraped from pipeline-url-link)
     *
     * @return None
     */
    var url = getDownloadableArtifactUrl(pipeline_id)
    console.log("Fetching data for pipeline: " + pipeline_id + " (" + url + ")")
    if (typeof pipeline_data[pipeline_id] != "undefined") {
        console.log("Metadata for pipeline: " + pipeline_id + " already fetched")
        extendRow(pipeline_id)
        return
    }
    pipeline_data[pipeline_id] = {}
    $.getJSON(url, function(json) {
        pipeline_data[pipeline_id].downloadable_artifacts = json;
        pipeline_data[pipeline_id].items = {};
        json.artifacts.forEach(function(artifact) {
            var job_id = getJobIDfromPath(artifact.path)
            var artifact_url = getArtifactUrl(job_id)
            console.log("Fetching __metadata__ artifact for pipeline: " + pipeline_id + " (Job: " + job_id + " - " + artifact_url + ")")
            $.getJSON(artifact_url, function(metadata) {
                var items = metadata.items
                for (const item in items) {
                    pipeline_data[pipeline_id].items[item] = items[item]
                }
                extendRow(pipeline_id)
            });
        });
    });
}

$(document).ready(function() {
    setTimeout(function () {
        getPipelineIDs().map(getDataForPipeline)
    }, 2000);
});