From f6cb8537d42909a6f6923c4e05e7c5f5c90a9493 Mon Sep 17 00:00:00 2001 From: Sayali Gaikawad Date: Tue, 30 Jan 2024 15:55:44 -0800 Subject: [PATCH] Add library to find and update GH issue labels Signed-off-by: Sayali Gaikawad --- .../TestUpdateGitHubIssueLabels.groovy | 157 ++++++++++++++++++ ...UpdateGitHubIssueLabels_Delete_Jenkinsfile | 26 +++ .../jobs/UpdateGitHubIssueLabels_Jenkinsfile | 26 +++ .../UpdateGitHubIssueLabels_Jenkinsfile.txt | 10 ++ ...pdateGitHubIssueLabels_Removal_Jenkinsfile | 26 +++ .../updateGitHubIssueLabelsLibTester.groovy | 52 ++++++ vars/updateGitHubIssueLabels.groovy | 101 +++++++++++ 7 files changed, 398 insertions(+) create mode 100644 tests/jenkins/TestUpdateGitHubIssueLabels.groovy create mode 100644 tests/jenkins/jobs/UpdateGitHubIssueLabels_Delete_Jenkinsfile create mode 100644 tests/jenkins/jobs/UpdateGitHubIssueLabels_Jenkinsfile create mode 100644 tests/jenkins/jobs/UpdateGitHubIssueLabels_Jenkinsfile.txt create mode 100644 tests/jenkins/jobs/UpdateGitHubIssueLabels_Removal_Jenkinsfile create mode 100644 tests/jenkins/lib-testers/updateGitHubIssueLabelsLibTester.groovy create mode 100644 vars/updateGitHubIssueLabels.groovy diff --git a/tests/jenkins/TestUpdateGitHubIssueLabels.groovy b/tests/jenkins/TestUpdateGitHubIssueLabels.groovy new file mode 100644 index 000000000..074a60c7d --- /dev/null +++ b/tests/jenkins/TestUpdateGitHubIssueLabels.groovy @@ -0,0 +1,157 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package jenkins.tests + + +import jenkins.tests.BuildPipelineTest +import org.junit.Before +import org.junit.Test +import static com.lesfurets.jenkins.unit.MethodCall.callArgsToString +import static org.hamcrest.CoreMatchers.hasItem +import static org.hamcrest.CoreMatchers.hasItems +import static org.hamcrest.CoreMatchers.not +import static org.hamcrest.MatcherAssert.assertThat +import static org.junit.jupiter.api.Assertions.assertThrows + + +class TestUpdateGitHubIssueLabels extends BuildPipelineTest { + + @Override + @Before + void setUp() { + super.setUp() + } + + @Test + void testIssueDoesNotExist() { + this.registerLibTester(new updateGitHubIssueLabelsLibTester( + "https://github.com/opensearch-project/opensearch-build", + "Test GH issue title", + "label101,label102", + "add" + )) + helper.addShMock("""gh issue list --repo https://github.com/opensearch-project/opensearch-build -S "Test GH issue title in:title" --json number --jq '.[0].number'""") { script -> + return [stdout: " ", exitValue: 0] + } + super.testPipeline('tests/jenkins/jobs/UpdateGitHubIssueLabels_Jenkinsfile') + assertThat(getCommands('println', ''), hasItem('No open issues found for https://github.com/opensearch-project/opensearch-build')) + } + + @Test + void testLabelCreation() { + this.registerLibTester(new updateGitHubIssueLabelsLibTester( + "https://github.com/opensearch-project/opensearch-build", + "Test GH issue title", + "label101,label102", + "add" + )) + helper.addShMock("""gh label list --repo https://github.com/opensearch-project/opensearch-build -S "label101" --json name --jq '.[0].name'""") { script -> + return [stdout: "no labels in opensearch-project/opensearch-build matched your search", exitValue: 0] + } + runScript('tests/jenkins/jobs/UpdateGitHubIssueLabels_Jenkinsfile') + assertThat(getCommands('sh', 'script'), hasItem("{script=gh label create label101 --repo https://github.com/opensearch-project/opensearch-build, returnStdout=true}")) + } + + @Test + void testExistingLabelsAddition() { + this.registerLibTester(new updateGitHubIssueLabelsLibTester( + "https://github.com/opensearch-project/opensearch-build", + "Test GH issue title", + "label101,label102", + "add" + )) + helper.addShMock("""gh issue list --repo https://github.com/opensearch-project/opensearch-build -S "Test GH issue title in:title" --json number --jq '.[0].number'""") { script -> + return [stdout: "67", exitValue: 0] + } + helper.addShMock("""gh label list --repo https://github.com/opensearch-project/opensearch-build -S label101 --json name --jq '.[0].name'""") { script -> + return [stdout: "label101", exitValue: 0] + } + helper.addShMock("""gh label list --repo https://github.com/opensearch-project/opensearch-build -S label102 --json name --jq '.[0].name'""") { script -> + return [stdout: "label102", exitValue: 0] + } + runScript('tests/jenkins/jobs/UpdateGitHubIssueLabels_Jenkinsfile') + assertThat(getCommands('sh', 'script'), not(hasItem('{script=gh label create label101 --repo https://github.com/opensearch-project/opensearch-build, returnStdout=true}'))) + assertThat(getCommands('sh', 'script'), not(hasItem('{script=gh label create label102 --repo https://github.com/opensearch-project/opensearch-build, returnStdout=true}'))) + assertThat(getCommands('sh', 'script'), hasItems('{script=gh issue edit 67 -R https://github.com/opensearch-project/opensearch-build --add-label \"label101\", returnStdout=true}', '{script=gh issue edit 67 -R https://github.com/opensearch-project/opensearch-build --add-label \"label102\", returnStdout=true}')) + } + + @Test + void testSkippingLabelCreationForRemove() { + this.registerLibTester(new updateGitHubIssueLabelsLibTester( + "https://github.com/opensearch-project/opensearch-build", + "Test GH issue title", + "label101", + "remove" + )) + helper.addShMock("""gh label list --repo https://github.com/opensearch-project/opensearch-build -S "label101" --json name --jq '.[0].name'""") { script -> + return [stdout: "no labels in opensearch-project/opensearch-build matched your search", exitValue: 0] + } + runScript('tests/jenkins/jobs/UpdateGitHubIssueLabels_Removal_Jenkinsfile') + assertThat(getCommands('println', 'label'), hasItem('label101 label does not exist. Skipping the label removal')) + } + + @Test + void testLabelRemoval() { + this.registerLibTester(new updateGitHubIssueLabelsLibTester( + "https://github.com/opensearch-project/opensearch-build", + "Test GH issue title", + "label101", + "remove" + )) + helper.addShMock("""gh issue list --repo https://github.com/opensearch-project/opensearch-build -S "Test GH issue title in:title" --json number --jq '.[0].number'""") { script -> + return [stdout: "67", exitValue: 0] + } + helper.addShMock("""gh label list --repo https://github.com/opensearch-project/opensearch-build -S label101 --json name --jq '.[0].name'""") { script -> + return [stdout: "label101", exitValue: 0] + } + runScript('tests/jenkins/jobs/UpdateGitHubIssueLabels_Removal_Jenkinsfile') + assertThat(getCommands('sh', 'script'), hasItem('{script=gh issue edit 67 -R https://github.com/opensearch-project/opensearch-build --remove-label \"label101\", returnStdout=true}')) + } + + @Test + void testFailure(){ + this.registerLibTester(new updateGitHubIssueLabelsLibTester( + "https://github.com/opensearch-project/opensearch-build", + "Test GH issue title", + "label101,label102", + "add" + )) + helper.addShMock("""gh issue list --repo https://github.com/opensearch-project/opensearch-build -S "Test GH issue title in:title" --json number --jq '.[0].number'""") { script -> + return [stdout: "Wrong credentials", exitValue: 127] + } + assertThrows(Exception) { + runScript('tests/jenkins/jobs/UpdateGitHubIssueLabels_Jenkinsfile') + } + assertThat(getCommands('error', ''), hasItem('Unable to edit GitHub issue for https://github.com/opensearch-project/opensearch-build, Script returned error code: 127')) + } + @Test + void testAction(){ + this.registerLibTester(new updateGitHubIssueLabelsLibTester( + "https://github.com/opensearch-project/opensearch-build", + "Test GH issue title", + "label101", + "delete" + )) + runScript('tests/jenkins/jobs/UpdateGitHubIssueLabels_Delete_Jenkinsfile') + assertThat(getCommands('error', ''), hasItem("Invalid action 'delete' specified. Valid values: add, remove")) + assertJobStatusFailure() + } + + def getCommands(method, text) { + def shCommands = helper.callStack.findAll { call -> + call.methodName == method + }.collect { call -> + callArgsToString(call) + }.findAll { command -> + command.contains(text) + } + return shCommands + } +} + diff --git a/tests/jenkins/jobs/UpdateGitHubIssueLabels_Delete_Jenkinsfile b/tests/jenkins/jobs/UpdateGitHubIssueLabels_Delete_Jenkinsfile new file mode 100644 index 000000000..9927a1182 --- /dev/null +++ b/tests/jenkins/jobs/UpdateGitHubIssueLabels_Delete_Jenkinsfile @@ -0,0 +1,26 @@ +/** + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + + The OpenSearch Contributors require contributions made to + this file be licensed under the Apache-2.0 license or a + compatible open source license. +*/ + +pipeline { + agent none + stages { + stage('updateGitHubIssueLabels') { + steps { + script { + updateGitHubIssueLabels( + repoUrl: "https://github.com/opensearch-project/opensearch-build", + issueTitle: "Test GH issue title", + label: "label101", + action: "delete" + ) + } + } + } + } +} diff --git a/tests/jenkins/jobs/UpdateGitHubIssueLabels_Jenkinsfile b/tests/jenkins/jobs/UpdateGitHubIssueLabels_Jenkinsfile new file mode 100644 index 000000000..75cbc0f34 --- /dev/null +++ b/tests/jenkins/jobs/UpdateGitHubIssueLabels_Jenkinsfile @@ -0,0 +1,26 @@ +/** + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + + The OpenSearch Contributors require contributions made to + this file be licensed under the Apache-2.0 license or a + compatible open source license. +*/ + +pipeline { + agent none + stages { + stage('updateGitHubIssueLabels') { + steps { + script { + updateGitHubIssueLabels( + repoUrl: "https://github.com/opensearch-project/opensearch-build", + issueTitle: "Test GH issue title", + label: "label101,label102", + action: "add" + ) + } + } + } + } +} diff --git a/tests/jenkins/jobs/UpdateGitHubIssueLabels_Jenkinsfile.txt b/tests/jenkins/jobs/UpdateGitHubIssueLabels_Jenkinsfile.txt new file mode 100644 index 000000000..56db63a53 --- /dev/null +++ b/tests/jenkins/jobs/UpdateGitHubIssueLabels_Jenkinsfile.txt @@ -0,0 +1,10 @@ + UpdateGitHubIssueLabels_Jenkinsfile.run() + UpdateGitHubIssueLabels_Jenkinsfile.pipeline(groovy.lang.Closure) + UpdateGitHubIssueLabels_Jenkinsfile.echo(Executing on agent [label:none]) + UpdateGitHubIssueLabels_Jenkinsfile.stage(updateGitHubIssueLabels, groovy.lang.Closure) + UpdateGitHubIssueLabels_Jenkinsfile.script(groovy.lang.Closure) + UpdateGitHubIssueLabels_Jenkinsfile.updateGitHubIssueLabels({repoUrl=https://github.com/opensearch-project/opensearch-build, issueTitle=Test GH issue title, label=label101,label102, action=add}) + updateGitHubIssueLabels.usernamePassword({credentialsId=jenkins-github-bot-token, passwordVariable=GITHUB_TOKEN, usernameVariable=GITHUB_USER}) + updateGitHubIssueLabels.withCredentials([[GITHUB_USER, GITHUB_TOKEN]], groovy.lang.Closure) + updateGitHubIssueLabels.sh({script=gh issue list --repo https://github.com/opensearch-project/opensearch-build -S "Test GH issue title in:title" --json number --jq '.[0].number', returnStdout=true}) + updateGitHubIssueLabels.println(No open issues found for https://github.com/opensearch-project/opensearch-build) diff --git a/tests/jenkins/jobs/UpdateGitHubIssueLabels_Removal_Jenkinsfile b/tests/jenkins/jobs/UpdateGitHubIssueLabels_Removal_Jenkinsfile new file mode 100644 index 000000000..18158dd85 --- /dev/null +++ b/tests/jenkins/jobs/UpdateGitHubIssueLabels_Removal_Jenkinsfile @@ -0,0 +1,26 @@ +/** + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + + The OpenSearch Contributors require contributions made to + this file be licensed under the Apache-2.0 license or a + compatible open source license. +*/ + +pipeline { + agent none + stages { + stage('updateGitHubIssueLabels') { + steps { + script { + updateGitHubIssueLabels( + repoUrl: "https://github.com/opensearch-project/opensearch-build", + issueTitle: "Test GH issue title", + label: "label101", + action: "remove" + ) + } + } + } + } +} diff --git a/tests/jenkins/lib-testers/updateGitHubIssueLabelsLibTester.groovy b/tests/jenkins/lib-testers/updateGitHubIssueLabelsLibTester.groovy new file mode 100644 index 000000000..559742488 --- /dev/null +++ b/tests/jenkins/lib-testers/updateGitHubIssueLabelsLibTester.groovy @@ -0,0 +1,52 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +import static org.hamcrest.CoreMatchers.notNullValue +import static org.hamcrest.CoreMatchers.nullValue +import static org.hamcrest.MatcherAssert.assertThat + +class updateGitHubIssueLabelsLibTester extends LibFunctionTester { + + private String repoUrl + private String issueTitle + private String label + private String action + + public updateGitHubIssueLabelsLibTester(repoUrl, issueTitle, label, action){ + this.repoUrl = repoUrl + this.issueTitle = issueTitle + this.label = label + this.action = action + } + + @Override + String libFunctionName() { + return 'updateGitHubIssueLabels' + } + + @Override + void parameterInvariantsAssertions(Object call) { + assertThat(call.args.repoUrl.first(), notNullValue()) + assertThat(call.args.issueTitle.first(), notNullValue()) + assertThat(call.args.label.first(), notNullValue()) + assertThat(call.args.action.first(), notNullValue()) + } + + @Override + boolean expectedParametersMatcher(Object call) { + return call.args.repoUrl.first().equals(this.repoUrl) + && call.args.issueTitle.first().equals(this.issueTitle) + && call.args.label.first().equals(this.label) + && call.args.action.first().equals(this.action) + } + + @Override + void configure(Object helper, Object binding) { + helper.registerAllowedMethod('withCredentials', [Map]) + } +} diff --git a/vars/updateGitHubIssueLabels.groovy b/vars/updateGitHubIssueLabels.groovy new file mode 100644 index 000000000..560f41f83 --- /dev/null +++ b/vars/updateGitHubIssueLabels.groovy @@ -0,0 +1,101 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +/** Library to edit GitHub issue labels across opensearch-project repositories. + @param Map args = [:] args A map of the following parameters + @param args.repoUrl - GitHub repository URL to create issue + @param args.issueTitle - GitHub issue title + @param args.label - Comma separated values of labels to add or remove from GitHub issue + @param args.action - Either remove or add the given label(s) + */ +void call(Map args = [:]) { + verifyActions(args.action) + try { + withCredentials([usernamePassword(credentialsId: 'jenkins-github-bot-token', passwordVariable: 'GITHUB_TOKEN', usernameVariable: 'GITHUB_USER')]) { + def issueNumber = sh( + script: "gh issue list --repo ${args.repoUrl} -S \"${args.issueTitle} in:title\" --json number --jq '.[0].number'", + returnStdout: true + ).trim() + if (!issueNumber.isEmpty()) { + switch (args.action) { + case ("add"): + addAction(args, issueNumber) + break + case ("remove"): + removeAction(args, issueNumber) + break + default: + error("Invalid action '${args.action}'. Valid values: add, remove") + } + } else { + println("No open issues found for ${args.repoUrl}") + } + } + } catch (Exception ex) { + error("Unable to edit GitHub issue for ${args.repoUrl}", ex.getMessage()) + } +} + +def addAction(args, issueNumber) { + List allLabels = Arrays.asList(args.label.split(',')) + allLabels.each { i -> + try { + def name = sh( + script: "gh label list --repo ${args.repoUrl} -S ${i} --json name --jq '.[0].name'", + returnStdout: true + ).trim() + if (name.equals(i.trim())) { + println("Label ${i} already exists. Adding it to the issue") + } else { + println("${i} label is missing. Creating the missing label") + sh( + script: "gh label create ${i} --repo ${args.repoUrl}", + returnStdout: true + ) + } + println("Adding ${i} label to the issue") + sh( + script: "gh issue edit ${issueNumber} -R ${args.repoUrl} --add-label \"${i}\"", + returnStdout: true + ) + } catch (Exception ex) { + error("Unable to create GitHub label for ${args.repoUrl}", ex.getMessage()) + } + } +} + +def removeAction(args, issueNumber){ + List allLabels = Arrays.asList(args.label.split(',')) + allLabels.each { i -> + try { + def name = sh( + script: "gh label list --repo ${args.repoUrl} -S ${i} --json name --jq '.[0].name'", + returnStdout: true + ).trim() + if (name.equals(i.trim())) { + println("Removing label ${i} from the issue") + sh( + script: "gh issue edit ${issueNumber} -R ${args.repoUrl} --remove-label \"${i}\"", + returnStdout: true + ) + } else { + println("${i} label does not exist. Skipping the label removal") + } + } catch (Exception ex) { + error("Unable to remove GitHub label for ${args.repoUrl}", ex.getMessage()) + } + } +} + +def verifyActions(String action) { + acceptableActions = ['add', 'remove'] + if (!acceptableActions.contains(action)){ + error("Invalid action '${action}' specified. Valid values: add, remove") + } +}