How to show a menu item on all projects

This guide will guide you through showing a menu item with associated URL and view on all projects' side panels, without requiring configuration in the job.

This could be used for example to show information about the job, such as statistics, without needing additional (persistently stored) data.

In this example, we’ll add a link called Statistics that will link to a page that shows some information about the project.

Create the action

The action will be fairly basic: It will expect a reference to an Project as constructor argument, and has getters returning the number of build steps and post-build steps this project has.

package org.jenkinsci.plugins.sample;

import hudson.model.Action;
import hudson.model.Project;

public class SampleAction implements Action {

    private Project project;

    public SampleAction(Project project) {
        this.project = project;
    }

    public int getBuildStepsCount() {
        return project.getBuilders().size();
    }

    public int getPostBuildStepsCount() {
        return project.getPublishersList().size();
    }

    @Override
    public String getIconFileName() {
        return "clipboard.png";
    }

    @Override
    public String getDisplayName() {
        return "Project Statistics";
    }

    @Override
    public String getUrlName() {
        return "stats";
    }
}

Similarly, the index.jelly view we create for it in the resource directory corresponding to this class — src/main/resources/org/jenkinsci/plugins/sample/SampleAction/ — is very basic, just showing the information from the getters:

<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:l="/lib/layout">
    <l:layout title="Project Statistics">
        <l:main-panel>
            <h1>
                Project Statistics
            </h1>
            <ul>
                <li>
                    Build Steps: ${it.buildStepsCount}
                </li>
                <li>
                    Post-Build Steps: ${it.postBuildStepsCount}
                </li>
            </ul>
        </l:main-panel>
    </l:layout>
</j:jelly>

Add the action to all projects

TransientActionFactory can be used to add any number of actions to a given instance of an Actionable subtype. TransientActionFactory defines:

  1. Which subtype of Actionable it applies to

  2. Which kinds of Action it creates

The class will look like this:

@Extension
public class SampleActionFactory extends TransientActionFactory<Project> {

    @Override
    public Class<Project> type() {
        return Project.class; (1)
    }

    @NonNull
    @Override
    public Collection<? extends Action> createFor(@NonNull Project project) {
        return Collections.singleton(new SampleAction(project)); (2)
    }
}
1 This will only apply to Project instances.
2 The factory could create different action depending on the Project, in this case, it is not needed.

Restrict access

To only show the project information to people who otherwise would be able to obtain it by viewing the job configuration, we can set up the action so the link is only shown to those with the Item.CONFIGURE permission.[1]

(...)
    @Override
    public String getIconFileName() {
        return this.project.hasPermission(Item.CONFIGURE) ? "clipboard.png" : null; (1)
    }
(...)
1 Returning null is a documented way for getIconFileName to make an action not appear in the side panel.

This will not prevent direct access via the URL however, so we need make sure to restrict who can access the action.

A reliable way to do this is to implement StaplerProxy, an interface intended to allow objects to forward HTTP request processing to another object. By implementing the getTarget() method and returning this, the request will continue to be processed by the same object, but we’re able to check user permissions before that happens.

(...)
import org.kohsuke.stapler.StaplerProxy;

public class SampleAction implements Action, StaplerProxy {
    (...)

    @Override
    public Object getTarget() {
        this.project.checkPermission(Item.CONFIGURE); (1)
        return this;
    }
}
1 This throws an AccessDeniedException if the check fails, resulting in the user seeing an error message (or, if not already logged in, a login screen).

1. Another option would be to only create the action for those with the correct permission. That approach would currently work for Jobs, but other objects in Jenkins use caching for actions so the transient actions are not recreated on every request. Of course, the chosen approach requires more sophisticated permission checks.