Skip to content

JENKINS-69312 JiraTestResultReporter plugin does not show plus button to create an issue #211

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Feb 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ junit (
autoUnlinkIssue: false,
additionalAttachments: false,
overrideResolvedIssues: false,
manualAddIssue: false,
)
]
)
Expand Down Expand Up @@ -123,6 +124,8 @@ If you check the **Auto unlink issues when test passes** check box, this plugin

If you check the **Auto override resolved issues** check box, this plugin will create new issues automatically for failing tests that are linked to already resolved issues.

If you check the **Manually link or raise issues** check box, this plugin will allow to manually link or create issues to the failing tests in new builds also if auto raise is not enabled.

![image of job config settings](img/job-config1.png)

Only after configuring the fields above, if you want you can override the **Summary** and **Description** values by clicking the **Advanced** button.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,165 +138,177 @@
return JobConfigMapping.getInstance().getOverrideResolvedIssues(getJobName());
}

public boolean getManualAddIssue() {
return JobConfigMapping.getInstance().getManualAddIssue(getJobName());
}

/**
* Getter for list of attachments by test method identified by its classname and name
* @param className
* @param name
* @return the list of attachments
*/
public List<String> getAttachments(String className, String name) {
if (!this.attachments.containsKey(className)) {
return Collections.emptyList();
}
Map<String, List<String>> attachmentsByClassname = this.attachments.get(className);
if (!attachmentsByClassname.containsKey(name)) {
return Collections.emptyList();
}
return attachmentsByClassname.get(name);
}

/**
* Getter for the project associated with this publisher
* @return
*/
private @CheckForNull AbstractProject<?, ?> getJobName() {
StaplerRequest2 currentRequest = Stapler.getCurrentRequest2();
return currentRequest != null ? currentRequest.findAncestorObject(AbstractProject.class) : null;
}

private boolean pipelineInvocation = false;
private JobConfigMapping.JobConfigEntry jobConfig;

private boolean isPipelineInvocation() {
return pipelineInvocation;
}

private JobConfigMapping.JobConfigEntry getJobConfig() {
return jobConfig;
}

@CheckForNull
public boolean getAdditionalAttachments() {
return jobConfig.getAdditionalAttachments();
}

@DataBoundSetter
public void setAdditionalAttachments(boolean additionalAttachments) {
JiraUtils.log(String.format("Additional attachments field configured as %s", additionalAttachments));
this.jobConfig = new JobConfigMapping.JobConfigEntryBuilder()
.withProjectKey(this.jobConfig.getProjectKey())
.withIssueType(this.jobConfig.getIssueType())
.withAutoRaiseIssues(this.jobConfig.getAutoRaiseIssue())
.withOverrideResolvedIssues(this.jobConfig.getOverrideResolvedIssues())
.withAutoResolveIssues(this.jobConfig.getAutoResolveIssue())
.withAutoUnlinkIssues(this.jobConfig.getAutoUnlinkIssue())
.withAdditionalAttachments(additionalAttachments)
.withOverrideResolvedIssues(this.jobConfig.getOverrideResolvedIssues())
.withManualAddIssues(this.jobConfig.getManualAddIssue())
.withConfigs(this.jobConfig.getConfigs())
.build();
}

/**
* Constructor
* @param configs a list with the configured fields
* @param projectKey
* @param issueType
* @param autoRaiseIssue
* @param autoResolveIssue
* @param autoUnlinkIssue
* @param overrideResolvedIssues
* @param manualAddIssue
*/
@DataBoundConstructor
public JiraTestDataPublisher(
List<AbstractFields> configs,
String projectKey,
String issueType,
boolean autoRaiseIssue,
boolean autoResolveIssue,
boolean autoUnlinkIssue,
boolean overrideResolvedIssues) {
boolean overrideResolvedIssues,
boolean manualAddIssue) {

long defaultIssueType;
try {
defaultIssueType = Long.parseLong(issueType);
} catch (NumberFormatException e) {
defaultIssueType = 1L;
}

this.jobConfig = new JobConfigMapping.JobConfigEntryBuilder()
.withProjectKey(projectKey)
.withIssueType(defaultIssueType)
.withAutoRaiseIssues(autoRaiseIssue)
.withOverrideResolvedIssues(overrideResolvedIssues)
.withAutoResolveIssues(autoResolveIssue)
.withAutoUnlinkIssues(autoUnlinkIssue)
.withOverrideResolvedIssues(overrideResolvedIssues)
.withManualAddIssues(manualAddIssue)
.withConfigs(Util.fixNull(configs))
.build();

if (Stapler.getCurrentRequest2() != null) {
// classic job - e.g. Freestyle project, Matrix project, etc.
AbstractProject<?, ?> project = Stapler.getCurrentRequest2().findAncestorObject(AbstractProject.class);
TestToIssueMapping.getInstance().register(project);
JobConfigMapping.getInstance().saveConfig(project, getJobConfig());
} else {
// pipeline invocation
pipelineInvocation = true;
}
}

/**
* Method invoked for contributing data to this run, see Jenkins documentation for details about arguments
* @param run
* @param workspace
* @param launcher
* @param listener
* @param testResult
* @return a JiraTestData object
* @throws IOException
* @throws InterruptedException
*/
@Override
public TestResultAction.Data contributeTestData(
Run<?, ?> run, @NonNull FilePath workspace, Launcher launcher, TaskListener listener, TestResult testResult)
throws IOException, InterruptedException {

EnvVars envVars = run.getEnvironment(listener);
Job<?, ?> job = run.getParent();
Job<?, ?> project;
if (job instanceof MatrixConfiguration) {
project = ((MatrixConfiguration) job).getParent();
} else {
project = job;
}

if (isPipelineInvocation()) {
TestToIssueMapping.getInstance().register(project);
JobConfigMapping.getInstance().saveConfig(project, getJobConfig());
}

boolean hasTestData = false;
if (JobConfigMapping.getInstance().getOverrideResolvedIssues(project)) {
hasTestData |= cleanJobCacheFile(listener, job, getTestCaseResults(testResult));
}

if (JobConfigMapping.getInstance().getAutoRaiseIssue(project)) {
if (JobConfigMapping.getInstance().getAdditionalAttachments(project)) {
JiraUtils.log("Obtaining junit-attachments ...");
GetTestDataMethodObject methodObject =
new GetTestDataMethodObject(run, workspace, launcher, listener, testResult);
this.attachments = methodObject.getAttachments();
JiraUtils.log("junit-attachments successfully retrieved");
}
hasTestData |= raiseIssues(listener, project, job, envVars, getTestCaseResults(testResult));
}

if (JobConfigMapping.getInstance().getAutoResolveIssue(project)) {
hasTestData |= resolveIssues(listener, project, job, envVars, getTestCaseResults(testResult));
}

if (JobConfigMapping.getInstance().getAutoUnlinkIssue(project)) {
hasTestData |= unlinkIssuesForPassedTests(listener, project, job, envVars, getTestCaseResults(testResult));
}

if (JobConfigMapping.getInstance().getManualAddIssue(project)) {
hasTestData |= true;

Check warning on line 309 in src/main/java/org/jenkinsci/plugins/JiraTestResultReporter/JiraTestDataPublisher.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 142-309 are not covered by tests
}

if (hasTestData) {
// Workaround to make feasible to use the publisher in parallel executions
if (!reportedTestDataBefore(envVars)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,11 +209,13 @@
return issueKeys;
}

IssueInput issueInput = JiraUtils.createIssueInput(job, test, envVars, JiraIssueTrigger.JOB);
SearchResult searchResult = JiraUtils.findIssues(job, test, envVars, issueInput);
if (searchResult != null && searchResult.getTotal() > 0) {
for (Issue issue : searchResult.getIssues()) {
issueKeys.add(issue.getKey());
if (!JobConfigMapping.getInstance().getManualAddIssue(job)) {

Check warning on line 212 in src/main/java/org/jenkinsci/plugins/JiraTestResultReporter/JiraUtils.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 212 is not covered by tests
IssueInput issueInput = JiraUtils.createIssueInput(job, test, envVars, JiraIssueTrigger.JOB);
SearchResult searchResult = JiraUtils.findIssues(job, test, envVars, issueInput);
if (searchResult != null && searchResult.getTotal() > 0) {
for (Issue issue : searchResult.getIssues()) {
issueKeys.add(issue.getKey());
}
}
}
return issueKeys;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
protected boolean autoResolveIssue;
protected boolean autoUnlinkIssue;
protected boolean additionalAttachments;
protected boolean manualAddIssue;
protected transient Pattern issueKeyPattern;

/**
Expand All @@ -77,401 +78,419 @@
boolean autoResolveIssue,
boolean autoUnlinkIssue,
boolean overrideResolvedIssues,
boolean additionalAttachments) {
boolean additionalAttachments,
boolean manualAddIssue) {
this.projectKey = projectKey;
this.issueType = issueType;
this.configs = configs;
this.autoRaiseIssue = autoRaiseIssue;
this.autoResolveIssue = autoResolveIssue;
this.autoUnlinkIssue = autoUnlinkIssue;
this.overrideResolvedIssues = overrideResolvedIssues;
this.additionalAttachments = additionalAttachments;
this.manualAddIssue = manualAddIssue;
compileIssueKeyPattern();
}

/**
* Getter for the issue type
* @return
*/
public Long getIssueType() {
return issueType;
}

/**
* Getter for the project key
* @return
*/
public String getProjectKey() {
return projectKey;
}

/**
* Getter for the configured fields
* @return
*/
public List<AbstractFields> getConfigs() {
return configs;
}

public boolean getAutoRaiseIssue() {
return autoRaiseIssue;
}

public boolean getAutoResolveIssue() {
return autoResolveIssue;
}

public boolean getAutoUnlinkIssue() {
return autoUnlinkIssue;
}

public boolean getAdditionalAttachments() {
return additionalAttachments;
}

public boolean getOverrideResolvedIssues() {
return overrideResolvedIssues;
}

public boolean getManualAddIssue() {
return manualAddIssue;
}

/**
* Getter for the issue key pattern
* @return
*/
public Pattern getIssueKeyPattern() {
return issueKeyPattern;
}

/**
* Method for resolving transient objects after deserialization. Called by the JVM.
* See Java documentation for more details.
* @return this object
*/
@SuppressFBWarnings(value = "SE_PRIVATE_READ_RESOLVE_NOT_INHERITED", justification = "TODO needs triage")
private Object readResolve() {
compileIssueKeyPattern();
return this;
}

protected void compileIssueKeyPattern() {
this.issueKeyPattern = projectKey != null ? Pattern.compile(projectKey + "-\\d+") : null;
}
}

/**
* Builder for a JobConfigEntry
*/
public static class JobConfigEntryBuilder extends JobConfigEntry {
/**
* Constructor
*/
public JobConfigEntryBuilder() {
super(null, null, new ArrayList<>(), false, false, false, false, false);
super(null, null, new ArrayList<>(), false, false, false, false, false, false);
}

public JobConfigEntryBuilder withProjectKey(String projectKey) {
this.projectKey = projectKey;
compileIssueKeyPattern();
return this;
}

public JobConfigEntryBuilder withIssueType(Long issueType) {
this.issueType = issueType;
return this;
}

public JobConfigEntryBuilder withConfigs(List<AbstractFields> configs) {
this.configs = configs;
return this;
}

public JobConfigEntryBuilder withAutoRaiseIssues(boolean autoRaiseIssues) {
this.autoRaiseIssue = autoRaiseIssues;
return this;
}

public JobConfigEntryBuilder withAutoResolveIssues(boolean autoResolveIssue) {
this.autoResolveIssue = autoResolveIssue;
return this;
}

public JobConfigEntryBuilder withAutoUnlinkIssues(boolean autoUnlinkIssues) {
this.autoUnlinkIssue = autoUnlinkIssues;
return this;
}

public JobConfigEntryBuilder withAdditionalAttachments(boolean additionalAttachments) {
this.additionalAttachments = additionalAttachments;
return this;
}

public JobConfigEntryBuilder withOverrideResolvedIssues(boolean overrideResolvedIssues) {
this.overrideResolvedIssues = overrideResolvedIssues;
return this;
}

public JobConfigEntryBuilder withManualAddIssues(boolean manualAddIssue) {
this.manualAddIssue = manualAddIssue;
return this;
}

public JobConfigEntry build() {
if (projectKey == null) {
throw new IllegalStateException("The Project Key may not be null");
}
if (issueType == null) {
throw new IllegalStateException("The Issue Type may not be null");
}
StringFields summary = null;
StringFields description = null;

for (AbstractFields field : this.getConfigs()) {
if (field instanceof StringFields) {
StringFields stringField = (StringFields) field;
if (stringField.getFieldKey().equals(SUMMARY_FIELD_NAME)) {
summary = stringField;
}
if (stringField.getFieldKey().equals(DESCRIPTION_FIELD_NAME)) {
description = stringField;
}
}
}

if (summary == null) {
this.getConfigs().add(DEFAULT_SUMMARY_FIELD);
}

if (description == null) {
this.getConfigs().add(DEFAULT_DESCRIPTION_FIELD);
}

return this;
}
}

private static JobConfigMapping instance = new JobConfigMapping();
private static final String CONFIGS_FILE = "JiraIssueJobConfigs";

/**
* Getter for the singleton instance
* @return
*/
public static JobConfigMapping getInstance() {
return instance;
}

private HashMap<String, JobConfigEntry> configMap;

/**
* Constructor. Will deserialize the existing map, or will create an empty new one
*/
private JobConfigMapping() {
configMap = new HashMap<String, JobConfigEntry>();

for (Job<?, ?> project : Jenkins.get().getItems(Job.class)) {
JobConfigEntry entry = load(project);
if (entry != null) {
configMap.put(project.getFullName(), entry);
}
}
}

/**
* Constructs the path for the config file
* @param project
* @return
*/
private String getPathToFile(Job<?, ?> project) {
return project.getRootDir().toPath().resolve(CONFIGS_FILE).toString();
}

private String getPathToJsonFile(Job<?, ?> project) {
return project.getRootDir().toPath().resolve(CONFIGS_FILE).toString() + ".json";
}

/**
* Looks for configurations from a previous version of the plugin and tries to load them
* and save them in the new format
* @param project
* @return the loaded JobConfigEntry, or null if there was no file, or it could not be loaded
*/
private JobConfigEntry loadBackwardsCompatible(Job<?, ?> project) {
try {
FileInputStream fileIn = new FileInputStream(getPathToFile(project));
ObjectInputStream in = new ObjectInputStream(fileIn);
JobConfigEntry entry = (JobConfigEntry) in.readObject();
in.close();
fileIn.close();
JiraUtils.log(
"Found and successfully loaded configs from a previous version for job: " + project.getFullName());
// make sure we have the configurations from previous versions in the new format
save(project, entry);
return entry;
} catch (FileNotFoundException e) {
// Nothing to do
} catch (Exception e) {
JiraUtils.logError(
"ERROR: Found configs from a previous version, but was unable to load them for project "
+ project.getFullName(),
e);
}
return null;
}

/**
* Loads the JobConfigEntry from the file associated with the project
* @param project
* @return the loaded JobConfigEntry, or null if there was no file, or it could not be loaded
*/
private JobConfigEntry load(Job<?, ?> project) {
JobConfigEntry entry = null;
try {
Gson gson = new GsonBuilder()
.registerTypeAdapter(AbstractFields.class, new FieldConfigsJsonAdapter())
.create();
FileInputStream fileIn = new FileInputStream(getPathToJsonFile(project));
JsonReader reader = new JsonReader(new InputStreamReader(fileIn, "UTF-8"));

entry = gson.fromJson(reader, JobConfigEntry.class);
reader.close();
fileIn.close();

return (JobConfigEntry) entry.readResolve();
} catch (FileNotFoundException e) {
entry = loadBackwardsCompatible(project);
if (entry == null) {
JiraUtils.log("No configs found for project " + project.getFullName());
}
} catch (Exception e) {
JiraUtils.logError("ERROR: Could not load configs for project " + project.getFullName(), e);
}
return entry;
}

/**
* Method for saving the map, called every time the map changes
*/
private void save(Job<?, ?> project, JobConfigEntry entry) {
try {
Gson gson = new GsonBuilder()
.registerTypeAdapter(AbstractFields.class, new FieldConfigsJsonAdapter())
.create();
FileOutputStream fileOut = new FileOutputStream(getPathToJsonFile(project));
JsonWriter writer = new JsonWriter(new OutputStreamWriter(fileOut, "UTF-8"));
writer.setIndent(" ");
gson.toJson(entry, JobConfigEntry.class, writer);
writer.close();
fileOut.close();
} catch (Exception e) {
JiraUtils.logError("ERROR: Could not save project map", e);
}
}

/**
* Method for setting the last configuration made for a project
* @param project
* @param projectKey
* @param issueType
* @param configs
*/
public synchronized void saveConfig(
Job<?, ?> project,
String projectKey,
Long issueType,
List<AbstractFields> configs,
boolean autoRaiseIssue,
boolean autoResolveIssue,
boolean autoUnlinkIssue,
boolean overrideResolvedIssues,
boolean additionalAttachments) {
boolean additionalAttachments,
boolean manualAddIssue) {
JobConfigEntry entry = new JobConfigEntry(
projectKey,
issueType,
configs,
autoRaiseIssue,
autoResolveIssue,
autoUnlinkIssue,
overrideResolvedIssues,
additionalAttachments);
additionalAttachments,
manualAddIssue);
saveConfig(project, entry);
}

/**
* Method for setting the last configuration made for a project
*/
public synchronized void saveConfig(Job<?, ?> project, JobConfigEntry entry) {
if (project == null) {
return;
}
if (entry instanceof JobConfigEntryBuilder) {
entry = ((JobConfigEntryBuilder) entry).build();
}
configMap.put(project.getFullName(), entry);
save(project, entry);
}

private JobConfigEntry getJobConfigEntry(@CheckForNull Job<?, ?> project) {
if (project == null) {
return null;
}
if (!configMap.containsKey(project.getFullName())) {
JobConfigEntry entry = load(project);
if (entry != null) {
configMap.put(project.getFullName(), entry);
}
}
return configMap.get(project.getFullName());
}

/**
* Getter for the last configured fields
* @param project
* @return
*/
public List<AbstractFields> getConfig(Job<?, ?> project) {
JobConfigEntry entry = getJobConfigEntry(project);
return entry != null ? entry.getConfigs() : null;
}

/**
* Getter for the last configured issue type
* @param project
* @return
*/
public Long getIssueType(Job<?, ?> project) {
JobConfigEntry entry = getJobConfigEntry(project);
return entry != null ? entry.getIssueType() : null;
}

/**
* Getter for the last configured project key
* @param project
* @return
*/
public String getProjectKey(Job<?, ?> project) {
JobConfigEntry entry = getJobConfigEntry(project);
return entry != null ? entry.getProjectKey() : null;
}

public boolean getAutoRaiseIssue(Job<?, ?> project) {
JobConfigEntry entry = getJobConfigEntry(project);
return entry != null ? entry.getAutoRaiseIssue() : false;
}

public boolean getAutoResolveIssue(Job<?, ?> project) {
JobConfigEntry entry = getJobConfigEntry(project);
return entry != null ? entry.getAutoResolveIssue() : false;
}

public boolean getAutoUnlinkIssue(Job<?, ?> project) {
JobConfigEntry entry = getJobConfigEntry(project);
return entry != null ? entry.getAutoUnlinkIssue() : false;
}

public boolean getAdditionalAttachments(Job<?, ?> project) {
JobConfigEntry entry = getJobConfigEntry(project);
return entry != null ? entry.getAdditionalAttachments() : false;
}

public boolean getOverrideResolvedIssues(Job<?, ?> project) {
JobConfigEntry entry = getJobConfigEntry(project);
return entry != null ? entry.getOverrideResolvedIssues() : false;
}

public boolean getManualAddIssue(Job<?, ?> project) {
JobConfigEntry entry = getJobConfigEntry(project);
return entry != null ? entry.getManualAddIssue() : false;

Check warning on line 491 in src/main/java/org/jenkinsci/plugins/JiraTestResultReporter/JobConfigMapping.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 82-491 are not covered by tests
}

/**
* Getter for the issue key pattern, used to validate user input
* @param project
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@
<f:checkbox/>
</f:entry>

<f:entry title="Manually link or raise issues" field="manualAddIssue">
<f:checkbox/>
</f:entry>

<f:advanced>
<j:set var="items" value="${ instance.configs != null ? instance.configs : descriptor.templates }"/>
<f:entry field="configs">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<div>
Allow issues to be manually linked via the jenkins webinterface for failing tests that aren't linked yet.
</div>