You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何修改artifactCleanup插件以保留仓库各目录的最后10个制品

需求:修改ArtifactCleanup插件实现按目录保留最新10个制品

我找到了artifactCleanup插件,已部署到Jenkins服务器,现在需要修改它的逻辑:原插件会清理过去X个月未被下载的所有制品,我希望改为在仓库及每个子目录中仅保留最后10个制品。

示例

原仓库结构

Main_Repo
   -folder1(含15个制品)
   -folder2(含20个制品)
   -art1
   -art2
   -...
   -art25

修改后需保留的结构

Main_Repo
   -folder1(含10个制品)
   -folder2(含10个制品)
   -art1
   -art2
   -...
   -art10

请问这是否可行?

当前插件主函数代码

private def artifactCleanup(String timeUnit, int timeInterval, String[] repos, log, paceTimeMS, dryRun = false, disablePropertiesSupport = false) {
    log.info "Starting artifact cleanup for repositories $repos, until $timeInterval ${timeUnit}s ago with pacing interval $paceTimeMS ms, dryrun: $dryRun, disablePropertiesSupport: $disablePropertiesSupport"

    cancelOnNullRepos(repos,'repos parameter must be specified before initiating cleanup.')

    // Create Map(repo, paths) of skiped paths (or others properties supported in future ...)
    def skip = [:]
    if ( ! disablePropertiesSupport && repos){
        skip = getSkippedPaths(repos)
    }

    def calendarUntil = Calendar.getInstance()

    calendarUntil.add(mapTimeUnitToCalendar(timeUnit), -timeInterval)

    def calendarUntilFormatted = new SimpleDateFormat("yyyy/MM/dd HH:mm").format(calendarUntil.getTime());
    log.info "Removing all artifacts not downloaded since $calendarUntilFormatted"

    Global.stopCleaning = false
    int cntFoundArtifacts = 0
    int cntNoDeletePermissions = 0
    long bytesFound = 0
    long bytesFoundWithNoDeletePermission = 0
    def artifactsCleanedUp = searches.artifactsNotDownloadedSince(calendarUntil, calendarUntil, repos)
    artifactsCleanedUp.find {
        try {
            while ( Global.pauseCleaning ) {
                log.info "Pausing by request"
                sleep( 60000 )
            }

            if ( Global.stopCleaning ) {
                log.info "Stopping by request, ending loop"
                return true
            }

            if ( ! disablePropertiesSupport && skip[ it.repoKey ] && StringUtils.startsWithAny(it.path, skip[ it.repoKey ])){
                if (log.isDebugEnabled()){
                    log.debug "Skip $it"
                }
                return false
            }

            bytesFound += repositories.getItemInfo(it)?.getSize()
            cntFoundArtifacts++
            if (!security.canDelete(it)) {
                bytesFoundWithNoDeletePermission += repositories.getItemInfo(it)?.getSize()
                cntNoDeletePermissions++
            }
            if (dryRun) {
                log.info "Found $it "
                log.info "\t==> currentUser: ${security.currentUser().getUsername()}"
                log.info "\t==> canDelete: ${security.canDelete(it)}"

            } else {
                if (security.canDelete(it)) {
                    log.info "Deleting $it"
                    repositories.delete it
                    log.info "Deleted..."
                } else {
                    log.info "Can't delete $it (user ${security.currentUser().getUsername()} has no delete permissions), "
                }
            }
        } catch (ItemNotFoundRuntimeException ex) {
            log.info "Failed to find $it, skipping"
        }

        def sleepTime = (Global.paceTimeMS > 0) ? Global.paceTimeMS : paceTimeMS
        if (sleepTime > 0) {
            sleep( sleepTime )
        }

        return false
    }

    if (dryRun) {
        log.info "Dry run - nothing deleted. Found $cntFoundArtifacts artifacts consuming $bytesFound bytes"
        if (cntNoDeletePermissions > 0) {
            log.info "$cntNoDeletePermissions artifacts cannot be deleted due to lack of permissions ($bytesFoundWithNoDeletePermission bytes)"
        }
    } else {
        log.info "Finished cleanup, deleting $cntFoundArtifacts artifacts that took up $bytesFound bytes"
        if (cntNoDeletePermissions > 0) {
            log.info "$cntNoDeletePermissions artifacts could not be deleted due to lack of permissions ($bytesFoundWithNoDeletePermission bytes)"
        }
    }
}

疑问点

我不确定如何找到searches对象,以及它调用的searches.artifactsNotDownloadedSince(calendarUntil, calendarUntil, repos)方法,希望得到帮助。


解答

可行性结论

这个需求完全可行,但需要替换原插件的核心搜索逻辑,不再基于下载时间筛选,而是按目录分组后保留最新的10个制品。

关于searches对象的说明

searches是Artifactory用户插件的内置上下文对象,属于Artifactory插件API的一部分,提供制品搜索相关的方法。原方法artifactsNotDownloadedSince是按下载时间范围筛选制品,但我们不需要这个逻辑,需要替换成遍历目录、分组筛选的逻辑。

修改思路

  1. 遍历目标仓库的所有目录(包括根目录)
  2. 对每个目录下的制品,按最后修改时间排序(最新的在前,可根据需求替换为创建时间)
  3. 保留排序后的前10个,删除其余制品
  4. 复用原插件的权限检查、dry-run、跳过路径等逻辑

核心代码修改示例

替换原插件中def artifactsCleanedUp = searches.artifactsNotDownloadedSince(...)及后续的遍历逻辑,改为以下逻辑:

// 初始化统计变量(和原插件保持一致)
Global.stopCleaning = false
int cntFoundArtifacts = 0
int cntNoDeletePermissions = 0
long bytesFound = 0
long bytesFoundWithNoDeletePermission = 0

// 遍历目标仓库
repos.each { repoKey ->
    // 获取仓库根目录下的所有子项(目录和制品)
    def rootItems = repositories.getChildren(repoKey, "")
    processDirectory(repoKey, "", rootItems, log, dryRun, skip, security, paceTimeMS)
}

// 递归处理目录的方法
private def processDirectory(String repoKey, String currentPath, def items, log, dryRun, skip, security, paceTimeMS) {
    def artifacts = []
    def directories = []

    // 分离当前目录下的制品和子目录
    items.each { item ->
        if (item.isFolder()) {
            directories.add(item)
        } else {
            artifacts.add(item)
        }
    }

    // 处理当前目录下的制品:排序后保留最新10个
    if (artifacts.size() > 10) {
        // 按最后修改时间降序排序
        def sortedArtifacts = artifacts.sort { -it.lastModified }
        // 要删除的是第11个及以后的制品
        def artifactsToDelete = sortedArtifacts.drop(10)

        artifactsToDelete.each { artifact ->
            def itemPath = "${currentPath}/${artifact.name}".replaceAll("//", "/")
            def itemId = "${repoKey}:${itemPath}"

            // 跳过配置中指定的路径
            if (skip[repoKey] && StringUtils.startsWithAny(itemPath, skip[repoKey])) {
                if (log.isDebugEnabled()) {
                    log.debug "Skip $itemId"
                }
                return
            }

            try {
                while (Global.pauseCleaning) {
                    log.info "Pausing by request"
                    sleep(60000)
                }

                if (Global.stopCleaning) {
                    log.info "Stopping by request, ending loop"
                    return true
                }

                def itemInfo = repositories.getItemInfo(itemId)
                long size = itemInfo?.getSize() ?: 0

                if (!security.canDelete(itemId)) {
                    log.info "Can't delete $itemId (user ${security.currentUser().getUsername()} has no delete permissions)"
                    cntNoDeletePermissions++
                    bytesFoundWithNoDeletePermission += size
                    return
                }

                cntFoundArtifacts++
                bytesFound += size

                if (dryRun) {
                    log.info "Found artifact to delete: $itemId"
                    log.info "\t==> canDelete: ${security.canDelete(itemId)}"
                } else {
                    log.info "Deleting $itemId"
                    repositories.delete(itemId)
                    log.info "Deleted..."
                }

                def sleepTime = (Global.paceTimeMS > 0) ? Global.paceTimeMS : paceTimeMS
                if (sleepTime > 0) {
                    sleep(sleepTime)
                }
            } catch (ItemNotFoundRuntimeException ex) {
                log.info "Failed to find $itemId, skipping"
            }
        }
    }

    // 递归处理子目录
    directories.each { dir ->
        def newPath = "${currentPath}/${dir.name}".replaceAll("//", "/")
        def dirItems = repositories.getChildren(repoKey, newPath)
        processDirectory(repoKey, newPath, dirItems, log, dryRun, skip, security, paceTimeMS)
    }
}

注意事项

  • 确保统计变量cntFoundArtifacts等在主函数开头正确初始化
  • 可根据需求调整排序逻辑(比如用creationDate代替lastModified)
  • 测试前务必开启dryRun模式,确认要删除的制品列表符合预期
  • 确保插件运行账号有足够的遍历和删除权限

内容的提问来源于stack exchange,提问作者Elydasian

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.09 12:44:54