defaults = [ action_type: ['print_branches'], action_help: '', version: '0.0.0', protect_branch: true ] if (env.BRANCH_NAME == 'master' || env.BRANCH_NAME.startsWith('hotfix/v')) { defaults.branch_type = 'hotfix' } if (env.BRANCH_NAME == 'develop' || env.BRANCH_NAME.startsWith('release/v')) { defaults.branch_type = 'release' } if (env.BRANCH_NAME == 'master' || env.BRANCH_NAME == 'develop') { defaults.action_type.add('start') defaults.action_type.add('protect') defaults.action_type.add('unprotect') } if (env.BRANCH_NAME ==~ /^(hotfix|release)\/v.+/) { defaults.action_type.add('merge') defaults.action_type.add('finish') defaults.action_type.add('rename') defaults.action_type.add('delete') defaults.action_type.add('protect') defaults.action_type.add('unprotect') } pipeline { agent { label 'branch_manager' } environment { GIT_SERVER = 'git.onlyoffice.com' GIT_OWNER = 'heatray' GITEA_TOKEN = credentials('gitea-token') TELEGRAM_TOKEN = credentials('telegram-bot-token') } options { disableConcurrentBuilds() } parameters { booleanParam ( name: 'wipe', description: 'Wipe out current workspace', defaultValue: false ) choice ( name: 'action_type', description: "Action type", choices: defaults.action_type ) string ( name: 'version', description: 'Release version (for start only) [' + defaults.branch_type + ']', defaultValue: defaults.version ) booleanParam ( name: 'protect_branch', description: 'Protect branch (for start & rename only)', defaultValue: defaults.protect_branch ) string ( name: 'extra_branch', description: 'Extra branch (for finish only)', defaultValue: '' ) booleanParam ( name: 'notify', description: 'Telegram notification', defaultValue: true ) } stages { stage('Branch Manager') { steps { script { currentBuild.displayName += ' - ' + params.action_type if (params.action_type == 'start') currentBuild.displayName += ' ' + params.version if (params.wipe) { deleteDir() checkout scm } stats = [repos: [:]] String branch = env.BRANCH_NAME ArrayList baseBranches = [] getRepos().each { stats.repos.put(it, [:]) } Boolean pAction Boolean sAction if (params.action_type == 'print_branches') { stats.repos.each { repo, status -> pAction = printBranches(repo) status.primary = (pAction) ? 'success' : 'failure' } } else if (params.action_type == 'start') { branch = defaults.branch_type + '/v' + params.version baseBranches = [env.BRANCH_NAME] stats.repos.each { repo, status -> if (checkRemoteBranch(repo, branch)) { echo "${repo}: Branch already ${branch} exists." status.primary = 'skip' } else { dir ('repos/' + repo) { checkoutRepo(repo) if (!checkRemoteBranch(repo, 'develop') && !createBranch(repo, 'develop', 'master')) error("Can't create develop branch.") pAction = createBranch(repo, branch, baseBranches[0]) status.primary = (pAction) ? 'success' : 'failure' } } if (params.protect_branch) { if (repo != 'documents-pipeline') { sAction = protectBranch(repo, branch) } else { sAction = false } status.secondary = (sAction) ? 'lock' : 'none' } } } else if (params.action_type == 'merge') { baseBranches = ['master'] stats.repos.each { repo, status -> if (!checkRemoteBranch(repo, branch)) { echo "${repo}: Branch doesn't ${branch} exist." status.primary = 'skip' } else { dir ('repos/' + repo) { checkoutRepo(repo) pAction = mergeBranch(repo, branch, baseBranches) status.primary = (pAction) ? 'success' : 'failure' } } } } else if (params.action_type == 'finish') { baseBranches = ['master', 'develop'] if (!params.extra_branch.isEmpty()) baseBranches.add(params.extra_branch) stats.repos.each { repo, status -> if (!checkRemoteBranch(repo, branch)) { echo "${repo}: Branch doesn't ${branch} exist." status.primary = 'skip' } else { dir ('repos/' + repo) { checkoutRepo(repo) unprotectBranch(repo, branch) pAction = mergeBranch(repo, branch, baseBranches) status.primary = (pAction) ? 'success' : 'failure' if (pAction) { if (repo != 'documents-pipeline') { sAction = deleteBranch(repo, branch) } else { sAction = false } status.secondary = (sAction) ? 'delete' : 'none' } } } } } else if (params.action_type == 'rename') { branch = defaults.branch_type + '/v' + params.version baseBranches = [env.BRANCH_NAME] stats.repos.each { repo, status -> if (checkRemoteBranch(repo, branch)) { echo "${repo}: Branch already ${branch} exists." status.primary = 'skip' } else { dir ('repos/' + repo) { checkoutRepo(repo, env.BRANCH_NAME) pAction = createBranch(repo, branch, env.BRANCH_NAME) status.primary = (pAction) ? 'success' : 'failure' if (pAction) { unprotectBranch(repo, env.BRANCH_NAME) deleteBranch(repo, env.BRANCH_NAME) if (params.protect_branch) { if (repo != 'documents-pipeline') { sAction = protectBranch(repo, branch) } else { sAction = false } status.secondary = (sAction) ? 'lock' : 'none' } } } } } } else if (params.action_type == 'delete') { stats.repos.each { repo, status -> if (!checkRemoteBranch(repo, branch)) { echo "${repo}: Branch doesn't ${branch} exist." status.primary = 'skip' } else { dir ('repos/' + repo) { checkoutRepo(repo, branch) unprotectBranch(repo, branch) if (repo != 'documents-pipeline') { pAction = deleteBranch(repo, branch) status.primary = (pAction) ? 'success' : 'failure' } } } } } else if (params.action_type == 'protect') { stats.repos.each { repo, status -> pAction = protectBranch(repo, branch) status.primary = (pAction) ? 'success' : 'failure' } } else if (params.action_type == 'unprotect') { stats.repos.each { repo, status -> pAction = unprotectBranch(repo, branch) status.primary = (pAction) ? 'success' : 'failure' } } stats.putAll([ branch: branch, baseBranches: baseBranches, success: stats.repos.findAll { repo, status -> status.primary in ['skip', 'success'] }.size(), total: stats.repos.size() ]) println stats } } } } post { success { script { if (stats.success == 0) currentBuild.result = 'FAILURE' else if (stats.success != stats.total) currentBuild.result = 'UNSTABLE' else if (stats.success == stats.total) currentBuild.result = 'SUCCESS' sendNotification() } } } } def getRepos() { return ['foo'] } def checkoutRepo(String repo, String branch = 'master') { sh ( label: "${repo}: checkout", script: """ if [ "\$(GIT_DIR=.git git rev-parse --is-inside-work-tree)" = 'true' ]; then git fetch --all --prune git switch -f ${branch} git reset --hard origin/${branch} git clean -df else rm -rfv ./* git clone -b ${branch} git@\$GIT_SERVER:\$GIT_OWNER/${repo}.git . fi git branch -vv """ ) } def checkRemoteBranch(String repo, String branch = 'master') { return sh ( label: "${repo}: check branch ${branch}", script: "git ls-remote --exit-code git@\$GIT_SERVER:\$GIT_OWNER/${repo}.git ${branch}", returnStatus: true ) == 0 } def createBranch(String repo, String branch, String baseBranch) { return sh ( label: "${repo}: start ${branch}", script: """ git switch ${baseBranch} git reset --hard origin/${baseBranch} git checkout -B ${branch} git push origin ${branch} git branch -vv echo "Branch created." """, returnStatus: true ) == 0 } def mergeBranch(String repo, String branch, ArrayList baseBranches) { return sh ( label: "${repo}: merge ${branch} into ${baseBranches.join(' ')}", script: """#!/bin/bash -xe git switch ${branch} git reset --hard origin/${branch} base_branches=(${baseBranches.join(' ')}) merged=0 rev_branch=\$(git rev-parse @) for base in "\${base_branches[@]}"; do git switch \$base || (((++merged)) && continue) git reset --hard origin/\$base rev_base=\$(git rev-parse @) if [[ \$rev_branch == \$rev_base ]]; then ((++merged)) echo "No new commits." continue fi # gh pr create --repo \$GIT_OWNER/${repo} --base \$base --head ${branch} \ # --title "Merge branch ${branch} into \$base" --fill || \ # true if ! git merge ${branch} --no-edit --no-ff \ -m "Merge branch ${branch} into \$base"; then git merge --abort continue fi git push origin \$base ((++merged)) done git branch -vv if [[ \$merged -ne \${#base_branches[@]} ]]; then echo "Not fully merged." exit 2 fi echo "Branch merged." """, returnStatus: true ) == 0 } def deleteBranch(String repo, String branch) { return sh ( label: "${repo}: delete ${branch}", script: """ git switch -f master git branch -D ${branch} git push --delete origin ${branch} echo "Branch deleted." """, returnStatus: true ) == 0 } def printBranches(String repo) { return sh ( label: "${repo}: branches list", script: """ HTTP_CODE=\$(curl -s -X 'GET' \ 'https://'"\$GIT_SERVER"'/api/v1/repos/'"\$GIT_OWNER"'/${repo}/branches' \ -H 'Authorization: token '"\$GITEA_TOKEN" \ -w '%{http_code}' \ -o output.json) test \$HTTP_CODE -eq 200 jq -r '.[] | [.name, .protected] | @tsv' output.json """, returnStatus: true ) == 0 } def protectBranch(String repo, String branch) { return sh ( label: "${repo}: protect ${branch}", script: """ HTTP_CODE=\$(echo '{ "branch_name": "${branch}", "enable_push": true, "enable_push_whitelist": true, "push_whitelist_usernames": [ "heatray" ] }' | \ curl -s -X 'POST' \ 'https://'"\$GIT_SERVER"'/api/v1/repos/'"\$GIT_OWNER"'/${repo}/branch_protections' \ -H 'Authorization: token '"\$GITEA_TOKEN" \ -H 'Content-Type: application/json' \ -w '%{http_code}' \ -o output.json \ -d @-) jq '.' output.json test \$HTTP_CODE -eq 201 """, returnStatus: true ) == 0 } def unprotectBranch(String repo, String branch) { String branchUrl = URLEncoder.encode(branch) return sh ( label: "${repo}: unprotect ${branch}", script: """ HTTP_CODE=\$(curl -s -X 'DELETE' \ 'https://'"\$GIT_SERVER"'/api/v1/repos/'"\$GIT_OWNER"'/${repo}/branch_protections/${branchUrl}' \ -H 'Authorization: token '"\$GITEA_TOKEN" \ -w '%{http_code}' \ -o output.json) jq '.' output.json test \$HTTP_CODE -eq 204 || test \$HTTP_CODE -eq 404 """, returnStatus: true ) == 0 } def sendNotification() { String text = '' switch(params.action_type) { case 'start': text = "Branch `${stats.branch}` created from `${stats.baseBranches[0]}`" break case 'merge': text = "Branch `${stats.branch}` merged into `${stats.baseBranches[0]}`" break case 'finish': text = "Branch `${stats.branch}` merged into " text += stats.baseBranches.collect({"`$it`"}).join(', ') break default: text = 'Stats' } text += " \\[${stats.success}/${stats.total}]" stats.repos.each { repo, status -> text += '\n' switch(status.primary) { case 'skip': text += '🔘'; break case 'success': text += '☑️'; break case 'failure': text += '🚫'; break default: text += '➖' } switch(status.secondary) { case 'lock': text += '🔒'; break case 'delete': text += '♻️'; break case 'none': text += '➖'; break default: text += '' } text += " [${repo}](https://${env.GIT_SERVER}/${env.GIT_OWNER}/${repo})" } echo text }