1

詹金斯:版本 1.525
詹金斯服务器 URLhttp ://my.jenkins.server.com:9040
Linux Red Hat 5.3

Artifactory:免费版
Artifactory 服务器网址http://my.artifactory.server:8081/Artifactory

我成功地在 Jenkins 中构建并将工件上传到我的 Artifactory 服务器下的相应存储库。

发生构建时,工件 (ProjectA-1.0.0.25.tar.gz) 会转到 libs-snapshot-local 存储库下的 Artifactory。这里 1.0.0 表示应用程序/詹金斯作业的给定版本的主要、次要和临时版本:在这种情况下为“ProjectA”。假设 25 是构建号


当 ProjectA 构建在开发中变得稳定时,我们将该应用程序版本的给定构建提升到 INT 或任何其他更高的环境(QA/PrePROD 等)。

在这个推广过程中,我们只要选择要推广的构建,使用 Jenkins Promoted Build Plugin,就可以成功。

现在,我们需要的是:

  1. 在升级过程中,我想调用一个 Groovy 脚本,它将在“ProjectA-1.0.0.25.tar.gz”成功后从 Jenkins 和 Artifactory (libs-snapshot-local) 中删除 ProjectA 的 1.0.0 版本的所有 Jenkins 构建晋升为INT。促销部分现在工作正常;我只需要一个 Groovy 脚本,它将删除 Jenkins 中的 Jenkins 构建(1.0.0.1 到 1.0.0.24 和 >= 1.0.0.25)及其来自 Artifactory 存储库(libs-snapshot-local)的相关工件。

我们公司的想法是,一旦为应用程序提升了发布版本的构建,我们拥有的所有其他构建/工件(在 Jenkins/Artifactory 中) - 我们希望使用 Groovy 脚本永久删除。有人会问,如果我想推广不同的构建怎么办#; 就我们而言,我们不希望那样。简单的规则是,如果有人推广 ProjectA-1.0.0.25.tar.gz,则在 Jenkins 和 Artifactory 中删除 ProjectA 的构建/工件,其中构建/工件不是 1.0.0.25,并继续使用新版本 1.1.0


具有以下功能的脚本会很棒。
1. 使用属性文件(jenkins.properties / artifactory.properties)——如果有的话,它将包含一些关于主机名/用户名/密码等的变量。
2. 使用 REST API 对给定应用程序/作业和给定版本执行删除(例如 1.0.0)
3. 可用于 Jenkins/Artifactory 删除 - 如果在命令提示符下,我说使用此 (Jenkins) 属性文件 - 或那个(Artifatory) - 在这两种情况下,应用程序及其发布值将是相同的。
4. 我们知道,为了将构建升级到 INT(使用 Jenkins 升级插件),我们将始终仅在 libs-snapshot-release 时从 Jenkins 服务器和 Artifactory 服务器中删除。
现在,如果有人(稍后)升级到 QA,那么工件存储库将是(libs-stage-local)

换句话说,我们应该调用 Groovy 脚本,传递一些变量/值 (REST) 并告诉要删除哪个应用程序/作业以及它是什么构建版本。然后,它将删除除用户将通过的版本(即 1.0.0.25)之外的所有版本

我是 Groovy / 使用 REST API 为 Jenkins/Artifactory 做这个“删除”工作的新手。如果有人已经有任何执行此类活动的示例脚本并且您可以共享,我将根据我的设置对其进行调整,看看我是否可以在促销步骤中看到上述行为。我在获得该脚本的工作版本方面有一些时间紧缩,希望有一些脚本代码执行相同的任务(而不是伟大的人告诉我浏览大量文档/链接,我知道这将使我成为 Groovy 中更好的编码器但它会延迟这篇文章的整个目的)。

非常感谢。

4

5 回答 5

0

找到了一种方法(此时不使用 REST API 调用,但很快我会更新,或者您可以提供帮助)。

解决方案 1 - 要删除 Jenkins 作业的所有构建,除了一个构建(我们选择用于提升)因此,在提升期间,我们将在 Jenkins 的 BUILD 部分下调用“scriptler”脚本,该脚本具有以下代码或创建一个单独的作业并通过传递 2 个参数(jobName 和 buildNumber - Jenkins 作业中的字符串参数)调用此脚本。

-bash-3.2$ cat bulkDeleteBuildsExceptOne.groovy

/*** BEGIN META {
  "name" : "Bulk Delete Builds except the given build number",
  "comment" : "For a given job and a given build numnber, delete all build except the user provided one.",
  "parameters" : [ 'jobName', 'buildNumber' ],
  "core": "1.410",
  "authors" : [
     { name : "Arun Sangal" }
  ]
} END META **/


// NOTE: Uncomment parameters below if not using Scriptler >= 2.0, or if you're just pasting the script in manually.
// ----- Logic in this script takes 5000 as the infinite number, decrease / increase this value from your own experience.
// The name of the job.
//def jobName = "some-job"

// The range of build numbers to delete.
//def buildNumber = "5"

def lastBuildNumber = buildNumber.toInteger() - 1;
def nextBuildNumber = buildNumber.toInteger() + 1;


import jenkins.model.*;
import hudson.model.Fingerprint.RangeSet;

def jij = jenkins.model.Jenkins.instance.getItem(jobName);

println("Keeping Job_Name: ${jobName} and build Number: ${buildNumber}");
println ""

def setBuildRange = "1-${lastBuildNumber}"

//println setBuildRange

def range = RangeSet.fromString(setBuildRange, true);

jij.getBuilds(range).each { it.delete() }

println("Builds have been deleted - Range: " + setBuildRange)


setBuildRange = "${nextBuildNumber}-5000"

//println setBuildRange

range = RangeSet.fromString(setBuildRange, true);

jij.getBuilds(range).each { it.delete() }

println("Builds have been deleted - Range: " + setBuildRange)

https://github.com/gigaaks/jenkins-scripts/blob/master/scriptler/bulkDeleteBuildsExceptOne.groovy - 或 http://scriptlerweb.appspot.com/script/show/101001(Scriptler 网站) - 这可以看到在远程目录脚本部分下的 Jenkins Scriptler 插件中。

如果 GITHUB 人员提供了一个简单的按钮/链接来推送我对 jenkinsci 主分支/存储库的更改,那会更容易一些。

虽然我仍在寻找两件事:

  1. 如何在 Groovy 中参数化以下脚本。使用 CliBuilder,我得到类未找到错误。
  2. 如何使用 Jenkins REST API 调用来做到这一点。稍后,我将使用 Artifactory REST API 调用来做同样的事情。
于 2013-09-19T01:43:24.010 回答
0

最终答案:这包括从 Artifactory 中删除构建工件以及使用 Artifactor 的 REST API 调用。此脚本将删除给定版本/版本的 Jenkins/Artifactory 构建/工件(有时随着时间的推移 - 给定的 Jenkins 作业可以创建多个版本/版本构建,例如:2.75.0.1、2.75.0.2、2.75.0.3。 ...,2.75.0.54, 2.76.0.1, 2.76.0.2, ..., 2.76.0.16, 2.76.1.1, 2.76.1.2, ...., 2.76.1.5)。在这种情况下,对于该作业的每个新版本,我们都会从 1 开始构建#。如果您必须删除除一个/甚至全部之外的所有构建(根据您自己的需要稍微更改脚本)并且不要更改较旧/其他版本的构建,请使用以下脚本。

Scriptler 目录链接http ://scriptlerweb.appspot.com/script/show/103001

享受!

/*** BEGIN META {
  "name" : "Bulk Delete Builds except the given build number",
  "comment" : "For a given job and a given build numnber, delete all builds of a given release version (M.m.interim) only and except the user provided one. Sometimes a Jenkins job use Build Name setter plugin and same job generates 2.75.0.1 and 2.76.0.43",
  "parameters" : [ 'jobName', 'releaseVersion', 'buildNumber' ],
  "core": "1.409",
  "authors" : [
     { name : "Arun Sangal - Maddys Version" }
  ]
} END META **/

import groovy.json.*
import jenkins.model.*;
import hudson.model.Fingerprint.RangeSet;
import hudson.model.Job;
import hudson.model.Fingerprint;

//these should be passed in as arguments to the script
if(!artifactoryURL) throw new Exception("artifactoryURL not provided")
if(!artifactoryUser) throw new Exception("artifactoryUser not provided")
if(!artifactoryPassword) throw new Exception("artifactoryPassword not provided")
def authString = "${artifactoryUser}:${artifactoryPassword}".getBytes().encodeBase64().toString()
def artifactorySettings = [artifactoryURL: artifactoryURL, authString: authString]

if(!jobName) throw new Exception("jobName not provided")
if(!buildNumber) throw new Exception("buildNumber not provided")

def lastBuildNumber = buildNumber.toInteger() - 1;
def nextBuildNumber = buildNumber.toInteger() + 1;

def jij = jenkins.model.Jenkins.instance.getItem(jobName);

def promotedBuildRange = new Fingerprint.RangeSet()
promotedBuildRange.add(buildNumber.toInteger())
def promoteBuildsList = jij.getBuilds(promotedBuildRange)
assert promoteBuildsList.size() == 1
def promotedBuild = promoteBuildsList[0]
// The release / version of a Jenkins job - i.e. in case you use "Build name" setter plugin in Jenkins for getting builds like 2.75.0.1, 2.75.0.2, .. , 2.75.0.15 etc.
// and over the time, change the release/version value (2.75.0) to a newer value i.e. 2.75.1 or 2.76.0 and start builds of this new release/version from #1 onwards.
def releaseVersion = promotedBuild.getDisplayName().split("\\.")[0..2].join(".")

println ""
println("- Jenkins Job_Name: ${jobName} -- Version: ${releaseVersion} -- Keep Build Number: ${buildNumber}");
println ""

/** delete the indicated build and its artifacts from artifactory */
def deleteBuildFromArtifactory(String jobName, int deleteBuildNumber, Map<String, String> artifactorySettings){
    println "     ## Deleting >>>>>>>>>: - ${jobName}:${deleteBuildNumber} from artifactory"
                                def artifactSearchUri = "api/build/${jobName}?buildNumbers=${deleteBuildNumber}&artifacts=1"
                                def conn = "${artifactorySettings['artifactoryURL']}/${artifactSearchUri}".toURL().openConnection()
                                conn.setRequestProperty("Authorization", "Basic " + artifactorySettings['authString']);
                                conn.setRequestMethod("DELETE")
    if( conn.responseCode != 200 ) {
        println "Failed to delete the build artifacts from artifactory for ${jobName}/${deleteBuildNumber}: ${conn.responseCode} - ${conn.responseMessage}"
    }
}

/** delete all builds in the indicated range that match the releaseVersion */
def deleteBuildsInRange(String buildRange, String releaseVersion, Job theJob, Map<String, String> artifactorySettings){
    def range = RangeSet.fromString(buildRange, true);
    theJob.getBuilds(range).each {
        if ( it.getDisplayName().find(/${releaseVersion}.*/)) {
            println "     ## Deleting >>>>>>>>>: " + it.getDisplayName();
            deleteBuildFromArtifactory(theJob.name, it.number, artifactorySettings)
            it.delete();
        }
    }
}

//delete all the matching builds before the promoted build number
deleteBuildsInRange("1-${lastBuildNumber}", releaseVersion, jij, artifactorySettings)

//delete all the matching builds after the promoted build number
deleteBuildsInRange("${nextBuildNumber}-${jij.nextBuildNumber}", releaseVersion, jij, artifactorySettings)

println ""
println("- Builds have been successfully deleted for the above mentioned release: ${releaseVersion}")
println "" 
于 2013-09-24T21:47:18.750 回答
0

好的。有一点调整。发现如果 Jenkins 作业从单个作业生成多个发布/版本构建/工件(即,如果它使用 Build Name Setter 插件)并使用 Major.minor.interim(例如 2.75.0)作为发布和生成构建此版本为 1-150,稍后一旦该版本进入 INT/QA 环境,同样的工作是为下一个版本(即 2.75.1 或 2.76.0 等)从第 1-N 个数字创建构建,然后是以下脚本可以解决问题。

请参阅此链接: 如果 Jenkins 构建被标记为“永久保留此构建”,请不要删除它 - Groovy 脚本删除 Jenkins 构建

bulkDeleteJenkinsBuildsExceptOne_OfAGivenRelease.groovy

/*** BEGIN META {
  "name" : "Bulk Delete Builds except the given build number",
  "comment" : "For a given job and a given build numnber, delete all builds of a given release version (M.m.interim) only and except the user provided one. Sometimes a Jenkins job use Build Name setter plugin and same job generates 2.75.0.1 and 2.76.0.43",
  "parameters" : [ 'jobName', 'releaseVersion', 'buildNumber' ],
  "core": "1.409",
  "authors" : [
     { name : "Arun Sangal" }
  ]
} END META **/


// NOTE: Uncomment parameters below if not using Scriptler >= 2.0, or if you're just pasting the script in manually.
// ----- Logic in this script takes 5000 as the infinite number, decrease / increase this value from your own experience.
// The name of the job.
//def jobName = "some-job"

// The release / version of a Jenkins job - i.e. in case you use "Build name" setter plugin in Jenkins for getting builds like 2.75.0.1, 2.75.0.2, .. , 2.75.0.15 etc.
// and over the time, change the release/version value (2.75.0) to a newer value i.e. 2.75.1 or 2.76.0 and start builds of this new release/version from #1 onwards.
//def releaseVersion = "2.75.0"

// The range of build numbers to delete.
//def buildNumber = "5"

def lastBuildNumber = buildNumber.toInteger() - 1;
def nextBuildNumber = buildNumber.toInteger() + 1;


import jenkins.model.*;
import hudson.model.Fingerprint.RangeSet;

def jij = jenkins.model.Jenkins.instance.getItem(jobName);
//def build = jij.getLastBuild();

println ""
println("- Jenkins Job_Name: ${jobName} -- Version: ${releaseVersion} -- Keep Build Number: ${buildNumber}");
println ""
println "  -- Range before given build number: ${buildNumber}"
println ""

def setBuildRange = "1-${lastBuildNumber}"
def range = RangeSet.fromString(setBuildRange, true);
jij.getBuilds(range).each {
  if ( it.getDisplayName().find(/${releaseVersion}.*/)) {
     println "     ## Deleting >>>>>>>>>: " + it.getDisplayName();

     // Trying to find - how to NOT delete a build in Jenkins if it's marked as "keep this build forever". If someone has an idea, please update this script with a newer version in GitHub.
     //if ( !build.isKeepLog()) {
          it.delete();
     //} else {
     //   println "build -- can't be deleted as :" + build.getWhyKeepLog();
     //}
  }
}



println ""
println "  -- Range after  given build number: ${buildNumber}"
println ""
setBuildRange = "${nextBuildNumber}-5000"
range = RangeSet.fromString(setBuildRange, true);
jij.getBuilds(range).each {
  if ( it.getDisplayName().find(/${releaseVersion}.*/)) {
     println "     ## Deleting >>>>>>>>>: " + it.getDisplayName();
     it.delete();
  }
}

println ""
println("- Builds have been successfully deleted for the above mentioned release: ${releaseVersion}")
println ""

在此处输入图像描述

于 2013-09-24T04:26:12.057 回答
0

对于使用 REST API 来调用上面的 scriptler 脚本或 Jenkins 工作,就像:想知道我在哪里传递 POST 操作。

主线是:
def artifactSearchUri = "api/build/${jobName}/${buildNumber}"
... 我们需要调整如下:

="api/build/Some_Jenkins_Job_That_You_Will_Create/buildWithParameters?jobName=Test_AppSvc&releaseVersion=2.75.0&buildNumber =15"

import groovy.json.*
def artifactoryURL= properties["jenkins.ARTIFACTORY_URL"]
def artifactoryUser = properties["artifactoryUser"]
def artifactoryPassword = properties["artifactoryPassword"]
def authString = "${artifactoryUser}:${artifactoryPassword}".getBytes().encodeBase64().toString()
def jobName = properties["jobName"]
def buildNumber = properties["buildNumber"]
def artifactSearchUri = "api/build/${jobName}/${buildNumber}"
def conn = "${artifactoryURL}/${artifactSearchUri}".toURL().openConnection()
conn.setRequestProperty("Authorization", "Basic " + authString);
println "Searching artifactory with: ${artifactSearchUri}"
def searchResults
if( conn.responseCode == 200 ) {
searchResults = new JsonSlurper().parseText(conn.content.text)
} else {
throw new Exception ("Failed to find the build info for ${jobName}/${buildNumber}: ${conn.responseCode} - ${conn.responseMessage}")
}

并且为了删除 Artifactory 构建,我们必须通过使用以下 groovy 脚本来组合上述逻辑,我仍在尝试使用它。我知道我很接近了。

博客: http ://browse.feedreader.com/c/Gridshore/11546011
脚本: https ://github.com/jettro/small-scripts/blob/master/groovy/artifactory/Artifactory.groovy

package artifactory

import groovy.text.SimpleTemplateEngine
import groovyx.net.http.RESTClient
import net.sf.json.JSON

/**
 * This groovy class is meant to be used to clean up your Atifactory server or get more information about it's
 * contents. The api of artifactory is documented very well at the following location
 * {@see http://wiki.jfrog.org/confluence/display/RTF/Artifactory%27s+REST+API}
 *
 * At the moment there is one major use of this class, cleaning your repository.
 *
 * Reading data about the repositories is done against /api/repository, if you want to remove items you need to use
 * '/api/storage'
 *
 * Artifactory returns a strange Content Type in the response. We want to use a generic JSON library. Therefore we need
 * to map the incoming type to the standard application/json. An example of the mapping is below, all the other
 * mappings can be found in the obtainServerConnection method.
 * 'application/vnd.org.jfrog.artifactory.storage.FolderInfo+json' => server.parser.'application/json'
 *
 * The class makes use of a config object. The config object is a map with a minimum of the following fields:
 * def config = [
 *       server: 'http://localhost:8080',
 *       repository: 'libs-release-local',
 *       versionsToRemove: ['/3.2.0-build-'],
 *       dryRun: true]
 *
 * The versionsToRemove is an array of strings that are the strart of builds that should be removed. To give an idea of
 * the build numbers we use: 3.2.0-build-1 or 2011.10-build-1. The -build- is important for the solution. This is how
 * we identify an artifact instead of a group folder.
 *
 * The final option to notice is the dryRun option. This way you can get an overview of what will be deleted. If set
 * to false, it will delete the selected artifacts.
 *
 * Usage example
 * -------------
 * def config = [
 *        server: 'http://localhost:8080',
 *        repository: 'libs-release-local',
 *        versionsToRemove: ['/3.2.0-build-'],
 *        dryRun: false]
 *
 * def artifactory = new Artifactory(config)
 *
 * def numberRemoved = artifactory.cleanArtifactsRecursive('nl/gridshore/toberemoved')
 *
 * if (config.dryRun) {*    println "$numberRemoved folders would have been removed."
 *} else {*    println "$numberRemoved folders were removed."
 *}* @author Jettro Coenradie
 */
private class Artifactory {
    def engine = new SimpleTemplateEngine()
    def config

    def Artifactory(config) {
        this.config = config
    }

    /**
     * Print information about all the available repositories in the configured Artifactory
     */
    def printRepositories() {
        def server = obtainServerConnection()
        def resp = server.get(path: '/artifactory/api/repositories')
        if (resp.status != 200) {
            println "ERROR: problem with the call: " + resp.status
            System.exit(-1)
        }
        JSON json = resp.data
        json.each {
            println "key :" + it.key
            println "type : " + it.type
            println "descritpion : " + it.description
            println "url : " + it.url
            println ""
        }
    }

    /**
     * Return information about the provided path for the configured  artifactory and server.
     *
     * @param path String representing the path to obtain information for
     *
     * @return JSON object containing information about the specified folder
     */
    def JSON folderInfo(path) {
        def binding = [repository: config.repository, path: path]
        def template = engine.createTemplate('''/artifactory/api/storage/$repository/$path''').make(binding)
        def query = template.toString()

        def server = obtainServerConnection()

        def resp = server.get(path: query)
        if (resp.status != 200) {
            println "ERROR: problem obtaining folder info: " + resp.status
            println query
            System.exit(-1)
        }
        return resp.data
    }

    /**
     * Recursively removes all folders containing builds that start with the configured paths.
     *
     * @param path String containing the folder to check and use the childs to recursively check as well.
     * @return Number with the amount of folders that were removed.
     */
    def cleanArtifactsRecursive(path) {
        def deleteCounter = 0
        JSON json = folderInfo(path)
        json.children.each {child ->
            if (child.folder) {
                if (isArtifactFolder(child)) {
                    config.versionsToRemove.each {toRemove ->
                        if (child.uri.startsWith(toRemove)) {
                            removeItem(path, child)
                            deleteCounter++
                        }
                    }
                } else {
                    if (!child.uri.contains("ro-scripts")) {
                        deleteCounter += cleanArtifactsRecursive(path + child.uri)
                    }
                }
            }
        }
        return deleteCounter
    }

    private RESTClient obtainServerConnection() {
        def server = new RESTClient(config.server)
        server.parser.'application/vnd.org.jfrog.artifactory.storage.FolderInfo+json' = server.parser.'application/json'
        server.parser.'application/vnd.org.jfrog.artifactory.repositories.RepositoryDetailsList+json' = server.parser.'application/json'

        return server
    }

    private def isArtifactFolder(child) {
        child.uri.contains("-build-")
    }

    private def removeItem(path, child) {
        println "folder: " + path + child.uri + " DELETE"
        def binding = [repository: config.repository, path: path + child.uri]
        def template = engine.createTemplate('''/artifactory/$repository/$path''').make(binding)
        def query = template.toString()
        if (!config.dryRun) {
            def server = new RESTClient(config.server)
            server.delete(path: query)
        }
    }
}
于 2013-09-24T04:31:23.097 回答
-1

首先,但它只是没有完成。为什么要删除:

  1. 一些不会伤害您的额外 Jenkins 构建。
  2. 从工件存储库(名为 Artifactory !!)中删除工件

话虽如此,我知道您可能仍然有充分的理由这样做(知道这会很有趣)。这是我可以提出的另一种方法:

  1. 詹金斯:我假设你正在使用 Maven。在这种情况下,您使用M2 发布插件来创建“发布版本”。现在这些构建将在它们旁边有一个特殊的手提箱图标,并将“永远保留这个构建”。您可以在 Jenkins 中尝试保留工件的天数、要保留的构建数量等,并制定自己的策略,以便满足您的要求。

  2. Artifactory:我使用 Nexus,所以实现可能会有所不同。但是您可以设置它,以便每次都覆盖快照构建。因此,您始终拥有n多个发布版本和恰好 1 个快照。第二个策略是“发布时删除快照”。这确保了相同编号的快照和版本不会在 repo 中共存。现在这正是它应该的样子,应该没有理由从像 Artifactory 这样的存储库中删除“已发布”的工件。这就是发布的全部意义所在。

于 2013-09-18T16:39:00.707 回答