Skip to content

Instantly share code, notes, and snippets.

@guykisel
Created January 28, 2019 19:23
Show Gist options
  • Save guykisel/da2750e77e9f4c0e9651a163a1b49ac4 to your computer and use it in GitHub Desktop.
Save guykisel/da2750e77e9f4c0e9651a163a1b49ac4 to your computer and use it in GitHub Desktop.
simplified jenkins p4-plugin wrapper
import groovy.transform.Field
// we want this to be global so that we can consistently
// sync the same changelist over the course of our pipeline
@Field currentChangelist = ''
def riotP4Sync(Map config = [:]) {
def humanReadableName = safePath("${JOB_NAME}-${STAGE_NAME}")
def jenkinsWorkspaceName = safePath("${JOB_NAME}-") + workspaceShortname(env.STAGE_NAME)
// we create a custom workspace to work around two issues:
// 1. this lets us make sure our workspace is safe for concurrent runs of the same pipeline on the same machine
// 2. this lets us have separate workspaces for each stage in a pipeline that does a sync
ws(jenkinsWorkspaceName) {
echo "[INFO] [riotP4Sync] Running in ${pwd()}"
// if we zero index then we never get a "1" workspace :(
def workspaceNumber = 1
// jenkins puts @<number> at the end of a workspace when running pipelines concurrently
if (pwd().contains('@')) {
workspaceNumber = pwd().split('@').last()
if (!workspaceNumber.isInteger()) {
error("${workspaceNumber} is not an integer! Something went wrong in riotP4Sync. PWD is ${pwd()}")
}
}
def p4WorkspaceName = safePath("${humanReadableName}-${workspaceNumber}")
echo "[INFO] [riotP4Sync] Using P4 Workspace ${p4WorkspaceName}"
def defaultConfig = [
workspacePattern: p4WorkspaceName,
syncType: 'AutoCleanImpl',
stream: '',
view: '',
quiet: true,
have: true,
charset: 'utf8',
parallel: true,
clobber: true,
]
def filesInWorkspace = true
try {
// see if we're in an empty dir
// an empty dir either means this is a brand new pipeline or
// we're running after someone manually deleted the dir as cleanup
filesInWorkspace = sh(returnStdout: true, script: 'ls').trim()
} catch (err) {
echo '[WARN] [riotP4Sync] checking for files in workspace failed'
}
if (!filesInWorkspace) {
// if we're in an empty dir, we should do a fresh sync.
defaultConfig.syncType = 'ForceCleanImpl'
}
if (riotCommon.workspaceIsEphemeral()) {
// if we're on a docker build node we don't care about existing files
// because our build node is ephemeral
defaultConfig.have = false
}
def requiredParams = ["credentialsId"]
def p4Config = riotCommon.overwriteDefaultConfig(defaultConfig, config, false)
if (!riotCommon.verifyConfig(p4Config, requiredParams, true)) {
error("Failed to provide a required parameter.")
}
if (!p4Config.view && !p4Config.stream) {
error("You must provide either a view spec or a stream.")
}
if (p4Config.view) {
p4Config.view = viewSpecWithWorkspace(p4Config.view, p4Config.workspacePattern)
}
echo '[INFO] [riotP4Sync] P4 Sync Config:'
def configArray = riotCommon.mapToArray(p4Config)
// do the actual sync
syncConfig(p4Config)
pwd()
}
}
def safePath(path) {
// remove characters that aren't alphanumeric
path.trim().replaceAll("[^a-zA-Z0-9]+","-")
}
def syncConfig(p4Config) {
// do the actual p4 sync using the p4 plugin
p4sync(
credential: p4Config.credentialsId,
populate: [$class: p4Config.syncType,
have: p4Config.have,
modtime: false,
parallel: [
enable: p4Config.parallel,
minbytes: '1024',
minfiles: '1',
path: '/usr/local/bin/p4',
threads: '4'
],
// specify the changelist to sync (syncs latest if nothing provided)
pin: currentP4Changelist(p4Config.changelist),
quiet: p4Config.quiet,
revert: true,
],
workspace: [$class: 'ManualWorkspaceImpl',
charset: p4Config.charset,
name: p4Config.workspacePattern,
pinHost: false,
spec: [allwrite: false,
clobber: p4Config.clobber,
compress: false,
line: 'LOCAL',
locked: false,
modtime: false,
rmdir: false,
streamName: p4Config.stream,
view: p4Config.view
]
]
)
// write the changelist we just synced back to the global changelist var
currentChangelist = env.P4_CHANGELIST
currentChangelist
}
// return the globally tracked p4 changelist for the current build
// returns empty string if we haven't synced yet
def currentP4Changelist(changelist) {
// intentionally using currentChangelist from global scope
if (changelist) {
// allow manually overriding the globally set changelist
currentChangelist = changelist
echo "[INFO] [currentP4Changelist] Changelist was specified: ${changelist}"
return currentChangelist
}
if (currentChangelist) {
echo "[INFO] [currentP4Changelist] Changelist was previously set: ${currentChangelist}"
return currentChangelist
}
echo "[INFO] [currentP4Changelist] Current changelist not found, will build most recent changelist."
''
}
String viewSpecWithWorkspace(viewspec, workspacePattern) {
def p4Viewspec = ''
if (viewspec) {
// Allow viewspec to be specified as an array. If so, join and insert workspace name
if ([Collection, Object[]].any { it.isAssignableFrom(viewspec.getClass()) }) {
p4Viewspec = viewspec.join("\n")
} else {
p4Viewspec = viewspec
}
// __WORKSPACE__ is a magic string we find and replace so that pipelines don't
// have to hardcode their workspace path
p4Viewspec = p4Viewspec.replace("__WORKSPACE__", workspacePattern)
}
p4Viewspec
}
def riotP4SyncContext(Map config = [:], Closure body) {
def jenkinsWorkspacePath = ''
def defaultConfig = [
stageName: 'P4 Sync'
]
def p4Config = riotCommon.overwriteDefaultConfig(defaultConfig, config, false)
stage(p4Config.stageName) {
try {
jenkinsWorkspacePath = riotP4Sync(p4Config)
} catch (err) {
// if we didn't force sync, and the sync failed, let's try again with a force sync if we're on a persistent node
if (p4Config.syncType != 'ForceCleanImpl' && !riotCommon.isOnDockerJenkins()) {
p4Config.syncType = 'ForceCleanImpl'
try {
jenkinsWorkspacePath = riotP4Sync(p4Config)
} catch (err2) {
throw err
}
} else {
throw err
}
}
}
echo "[INFO] [riotP4SyncContext] P4 Sync complete, running in ${jenkinsWorkspacePath}"
ws(jenkinsWorkspacePath) {
// run the closure in the custom workspace
body()
}
jenkinsWorkspacePath
}
// return a four character string representing the workspace
def workspaceShortname(string) {
def safeString = safePath(string)
try {
return md5sum(safeString)[0..3]
} catch (err) {
try {
// as a fallback just grab first two and last two chars
return safeString[0..1] + safeString[-2..-1]
} catch (err2) {
return safeString
}
}
}
def md5sum(string) {
// md5sum works on windows and linux, but md5 is for os x
return sh(returnStdout: true, script: "echo -n ${string} | md5sum || echo -n ${string} | md5").trim()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment