├── .travis.yml ├── vars ├── submitToActiveData.groovy ├── submitToActiveData.txt ├── publishToS3.txt ├── ircNotification.txt ├── writeCapabilities.txt ├── publishToPulse.txt ├── writeCapabilities.groovy ├── publishToPulse.groovy ├── ircNotification.groovy ├── submitToTreeherder.txt ├── publishToS3.groovy └── submitToTreeherder.groovy ├── CODE_OF_CONDUCT.md ├── src └── org │ └── mozilla │ └── fxtest │ ├── Pulse.groovy │ ├── JsonSchemaValidator.groovy │ └── ServiceBook.groovy ├── tests ├── serviceBookTests.groovy └── submitToTreeherderTests.groovy ├── README.md └── resources └── org └── mozilla └── fxtest └── pulse └── schemas └── treeherder.yml /.travis.yml: -------------------------------------------------------------------------------- 1 | language: groovy 2 | jdk: 3 | - oraclejdk8 4 | -------------------------------------------------------------------------------- /vars/submitToActiveData.groovy: -------------------------------------------------------------------------------- 1 | /** Submit structured logs to ActiveData 2 | * 3 | * @param logPath path to the structured log(s) 4 | */ 5 | def call(String logPath) { 6 | publishToS3(logPath, 'net-mozaws-stage-fx-test-activedata') 7 | } 8 | -------------------------------------------------------------------------------- /vars/submitToActiveData.txt: -------------------------------------------------------------------------------- 1 |

2 | Submit structured logs to 3 | ActiveData. 4 |

5 |
6 |
submitToActiveData(logPath)
7 |
8 | Publishes the structured log(s) at logPath to ActiveData. 9 |
10 | -------------------------------------------------------------------------------- /vars/publishToS3.txt: -------------------------------------------------------------------------------- 1 |

2 | Publish files to an Amazon S3 3 | bucket. 4 |

5 |
6 |
publishToS3(path, bucket, [region])
7 |
8 | Publishes the files at path to the specified S3 9 | bucket and region. Defaults to region 10 | us-east-1. 11 |
12 | -------------------------------------------------------------------------------- /vars/ircNotification.txt: -------------------------------------------------------------------------------- 1 |

Sends a notice to IRC with the current build result.

2 |
3 |
ircNotification([channel, nick, server])
4 |
5 | Sends a notification to IRC with the specified channel, 6 | nick, and server. By default it will connect to 7 | irc.mozilla.org:6697 as fxtest and join the 8 | #fx-test-alerts channel. 9 |
10 | -------------------------------------------------------------------------------- /vars/writeCapabilities.txt: -------------------------------------------------------------------------------- 1 |

Write desired capabilities to a JSON file.

2 |
3 |
writeCapabilities(capabilities, [path])
4 |
5 | Writes a JSON file containing the items from the capabilities 6 | map to the specified path (for use by 7 | 8 | pytest-selenium). If omitted, the path defaults to 9 | capabilities.json in the working directory. 10 |
11 |
12 | -------------------------------------------------------------------------------- /vars/publishToPulse.txt: -------------------------------------------------------------------------------- 1 |

2 | Publish message to a 3 | Pulse 4 | exchange.

5 |
6 |
publishToPulse(exchange, routingKey, message, [schema])
7 |
8 | Publishes the message, to Pulse with the specified 9 | exchange and routingKey. If a schema is provided 10 | then it will be used to check that the message is valid. If the message 11 | fails to pass validation, details will be output to the console log and 12 | ProcessingException will be thrown. 13 |
14 | -------------------------------------------------------------------------------- /vars/writeCapabilities.groovy: -------------------------------------------------------------------------------- 1 | import groovy.json.JsonOutput 2 | 3 | /** Write capabilities to JSON file 4 | * 5 | * @param desiredCapabilities capabilities to include in the file 6 | * @param path destination for capabilities file 7 | */ 8 | def call(Map desiredCapabilities, String path = 'capabilities.json') { 9 | defaultCapabilities = [ 10 | build: env.BUILD_TAG, 11 | public: 'public restricted' 12 | ] 13 | capabilities = defaultCapabilities.clone() 14 | capabilities.putAll(desiredCapabilities) 15 | json = JsonOutput.toJson([capabilities: capabilities]) 16 | writeFile file: path, text: json 17 | } 18 | -------------------------------------------------------------------------------- /vars/publishToPulse.groovy: -------------------------------------------------------------------------------- 1 | /** Publish message to a Pulse exchange 2 | * 3 | * @param exchange exchange to send message to 4 | * @param routingKey routing key to use 5 | * @param message message to send 6 | * @param schema optional schema to validate against 7 | */ 8 | def call(String exchange, 9 | String routingKey, 10 | String message, 11 | String schema = null) { 12 | if ( schema != null ) { 13 | def jsonSchemaValidator = new org.mozilla.fxtest.JsonSchemaValidator() 14 | jsonSchemaValidator.validate(message, schema) 15 | } 16 | def pulse = new org.mozilla.fxtest.Pulse() 17 | pulse.publish(exchange, routingKey, message) 18 | } 19 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Community Participation Guidelines 2 | 3 | This repository is governed by Mozilla's code of conduct and etiquette guidelines. 4 | For more details, please read the 5 | [Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/). 6 | 7 | ## How to Report 8 | For more information on how to report violations of the Community Participation Guidelines, please read our '[How to Report](https://www.mozilla.org/about/governance/policies/participation/reporting/)' page. 9 | 10 | 16 | -------------------------------------------------------------------------------- /vars/ircNotification.groovy: -------------------------------------------------------------------------------- 1 | /** Send a notice to IRC with the current build result 2 | * 3 | * @param channel channel to join 4 | * @param nick nickname to use 5 | * @param server server to connect to 6 | */ 7 | def call(String channel = '#fx-test-alerts', 8 | String nick = 'fxtest', 9 | String server = 'irc.mozilla.org:6697') { 10 | nick = "${nick}-${BUILD_NUMBER}" 11 | result = currentBuild.result ?: 'SUCCESS' 12 | message = "Project ${JOB_NAME} build #${BUILD_NUMBER}: ${result}: ${BUILD_URL}" 13 | sh """ 14 | ( 15 | echo NICK ${nick} 16 | echo USER ${nick} 8 * : ${nick} 17 | sleep 5 18 | echo "JOIN ${channel}" 19 | echo "NOTICE ${channel} :${message}" 20 | echo QUIT 21 | ) | openssl s_client -connect ${server} 22 | """ 23 | } 24 | -------------------------------------------------------------------------------- /vars/submitToTreeherder.txt: -------------------------------------------------------------------------------- 1 |

2 | Submit build results to 3 | Treeherder. 4 |

5 |
6 |
7 | submitToTreeherder(project, jobSymbol, jobName, [artifactPath, logPath, 8 | groupSymbol, groupName]) 9 |
10 |
11 | Submits the build result for project to Treeherder using the 12 | specified jobSymbol and jobName. If provided, 13 | files located by artifactPath and logPath will be 14 | published to Amazon S3 and linked from the build results. By default the 15 | job will have a groupSymbol of 'j', and a 16 | groupName of 'Executed by Jenkins'. To leave the job ungrouped 17 | pass a groupSymbol of '?'. 18 |
19 | -------------------------------------------------------------------------------- /src/org/mozilla/fxtest/Pulse.groovy: -------------------------------------------------------------------------------- 1 | package org.mozilla.fxtest 2 | 3 | @Grab(group='com.rabbitmq', module='amqp-client', version='4.1.0') 4 | import com.rabbitmq.client.AMQP 5 | import com.rabbitmq.client.ConnectionFactory 6 | import com.rabbitmq.client.MessageProperties 7 | 8 | @NonCPS 9 | def publish(String exchange, String routingKey, String payload) { 10 | def factory = new ConnectionFactory() 11 | factory.setUri("amqp://${PULSE}@pulse.mozilla.org:5671") 12 | factory.useSslProtocol() 13 | def connection = factory.newConnection() 14 | def channel = connection.createChannel() 15 | channel.exchangeDeclare exchange, 'topic', true 16 | 17 | def properties = new AMQP.BasicProperties.Builder() 18 | .contentType('application/json') 19 | .deliveryMode(2) 20 | .build() 21 | 22 | channel.basicPublish exchange, routingKey, properties, payload.bytes 23 | echo "Published payload to Pulse on $exchange with routing key $routingKey." 24 | echo payload 25 | channel.close() 26 | connection.close() 27 | } 28 | -------------------------------------------------------------------------------- /vars/publishToS3.groovy: -------------------------------------------------------------------------------- 1 | /** Publish files to an Amazon S3 bucket 2 | * 3 | * @param path path to the file(s) to publish 4 | * @param bucket bucket and destination for file(s) 5 | * @param region region bucket belongs to 6 | * @return list of [name, url] maps for published file(s) 7 | */ 8 | def call(String path, String bucket, String region = 'us-east-1') { 9 | step([$class: 'S3BucketPublisher', 10 | consoleLogLevel: 'INFO', 11 | dontWaitForConcurrentBuildCompletion: false, 12 | entries: [[ 13 | bucket: "$bucket/${BUILD_TAG}", 14 | excludedFile: '', 15 | flatten: true, 16 | gzipFiles: true, 17 | keepForever: false, 18 | managedArtifacts: false, 19 | noUploadOnFailure: false, 20 | selectedRegion: region, 21 | showDirectlyInBrowser: false, 22 | sourceFile: path, 23 | storageClass: 'STANDARD', 24 | uploadFromSlave: false, 25 | useServerSideEncryption: false]], 26 | pluginFailureResultConstraint: 'SUCCESS', 27 | profileName: 'fx-test-jenkins-s3-publisher']) 28 | return getLinks(bucket, path) 29 | } 30 | 31 | def getLinks(bucket, path) { 32 | def links = [] 33 | def files = findFiles(glob: path) 34 | for ( f in files ) { 35 | links.add([ 36 | name: f.name, 37 | url: "https://s3.amazonaws.com/$bucket/${BUILD_TAG}/$f.name"]) 38 | } 39 | return links 40 | } 41 | -------------------------------------------------------------------------------- /src/org/mozilla/fxtest/JsonSchemaValidator.groovy: -------------------------------------------------------------------------------- 1 | package org.mozilla.fxtest 2 | 3 | @Grab(group='com.fasterxml.jackson.core', module='jackson-databind', version='2.8.8') 4 | import com.fasterxml.jackson.databind.ObjectMapper 5 | 6 | @Grab(group='com.fasterxml.jackson.dataformat', module='jackson-dataformat-yaml', version='2.8.3') 7 | import com.fasterxml.jackson.dataformat.yaml.YAMLFactory 8 | 9 | @Grab(group='com.github.fge', module='json-schema-validator', version='2.2.5') 10 | import com.github.fge.jackson.JsonLoader 11 | import com.github.fge.jsonschema.core.exceptions.ProcessingException 12 | import com.github.fge.jsonschema.main.JsonSchemaFactory 13 | 14 | @NonCPS 15 | def validate(payload, schema) { 16 | def mapper = new ObjectMapper() 17 | def yamlFactory = new YAMLFactory() 18 | def jsonSchemaFactory = JsonSchemaFactory.byDefault() 19 | def schemaJsonNode = mapper.readTree(yamlFactory.createParser(schema)) 20 | def jsonSchema = jsonSchemaFactory.getJsonSchema(schemaJsonNode) 21 | def payloadJsonNode = mapper.readTree(payload) 22 | def report = jsonSchema.validate(payloadJsonNode) 23 | if ( !report.isSuccess() ) { 24 | for ( message in report ) { 25 | echo "$message" 26 | } 27 | throw new ProcessingException('Failure validating Pulse payload against schema.') 28 | } else { 29 | echo 'Sucessfully validated Pulse payload against schema.' 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/serviceBookTests.groovy: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import org.junit.Before 4 | import org.junit.Test 5 | 6 | import com.lesfurets.jenkins.unit.BasePipelineTest 7 | 8 | class ServiceBookTests extends BasePipelineTest { 9 | 10 | 11 | @Override 12 | @Before 13 | void setUp() throws Exception { 14 | super.setUp() 15 | String fileContents = new File('tests/projects.json').text 16 | def resp = [content: fileContents] 17 | helper.registerAllowedMethod('httpRequest', [String.class], {url -> resp}) 18 | } 19 | 20 | @Test 21 | void allowedOrgs() { 22 | def script = loadScript('src/org/mozilla/fxtest/ServiceBook.groovy') 23 | for ( i in ['Kinto', 'mozilla', 'mozilla-services'] ) { 24 | assert script.validURL("https://github.com/${i}/foo") == true 25 | } 26 | } 27 | 28 | @Test 29 | void disallowedOrgs() { 30 | def script = loadScript('src/org/mozilla/fxtest/ServiceBook.groovy') 31 | for ( i in ['kinto', 'Mozilla', 'davehunt'] ) { 32 | assert script.validURL("https://github.com/${i}/foo") == false 33 | } 34 | } 35 | 36 | @Test 37 | void projectWithTests() { 38 | def script = loadScript('src/org/mozilla/fxtest/ServiceBook.groovy') 39 | 40 | // kinto has 1 jenkins enabled project 41 | def tests = script.getProjectTests('kinto') 42 | assert tests != null 43 | assert tests.size() == 1 44 | 45 | // this project does not exists 46 | assert script.getProjectTests('IDONTEXIST') == null 47 | 48 | // Balrog has zero jenkins tests 49 | def balrog = script.getProjectTests('Balrog') 50 | assert balrog.size() == 0 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/submitToTreeherderTests.groovy: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import org.junit.Before 4 | import org.junit.Test 5 | 6 | import com.lesfurets.jenkins.unit.BasePipelineTest 7 | 8 | class SubmitToTreeherderTests extends BasePipelineTest { 9 | 10 | 11 | @Override 12 | @Before 13 | void setUp() throws Exception { 14 | super.setUp() 15 | } 16 | 17 | @Test 18 | void defaultGroup() { 19 | def script = loadScript('vars/submitToTreeherder.groovy') 20 | def display = script.getDisplay('myJobSymbol', 'myJobName') 21 | assert display.groupSymbol == 'j' 22 | assert display.groupName == 'Executed by Jenkins' 23 | } 24 | 25 | @Test 26 | void customGroup() { 27 | def script = loadScript('vars/submitToTreeherder.groovy') 28 | def display = script.getDisplay('myJobSymbol', 'myJobName', 'myGroupSymbol', 'myGroupName') 29 | assert display.groupSymbol == 'myGroupSymbol' 30 | assert display.groupName == 'myGroupName' 31 | } 32 | 33 | @Test 34 | void noGroup() { 35 | def script = loadScript('vars/submitToTreeherder.groovy') 36 | def display = script.getDisplay('myJobSymbol', 'myJobName', '?', null) 37 | assert display.groupSymbol == '?' 38 | assert !display.containsKey('groupName') 39 | } 40 | 41 | @Test 42 | void getLogsReturnsListOfLinkMaps() { 43 | helper.registerAllowedMethod('publishToS3', [String.class, String.class], {path, bucket -> 44 | [[url:'linkURL', name:'linkName']]}) 45 | def script = loadScript('vars/submitToTreeherder.groovy') 46 | def links = script.getLogs('foo') 47 | assert links.size() == 1 48 | assert links[0].url == 'linkURL' 49 | assert links[0].name == 'linkName' 50 | } 51 | 52 | @Test 53 | void tbplLogNameIsModified() { 54 | helper.registerAllowedMethod('publishToS3', [String.class, String.class], {path, bucket -> 55 | [[url:'linkURL', name:'my-tbpl-log']]}) 56 | def script = loadScript('vars/submitToTreeherder.groovy') 57 | def links = script.getLogs('foo') 58 | assert links.size() == 1 59 | assert links[0].url == 'linkURL' 60 | assert links[0].name == 'buildbot_text' 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/org/mozilla/fxtest/ServiceBook.groovy: -------------------------------------------------------------------------------- 1 | package org.mozilla.fxtest 2 | import groovy.json.JsonSlurperClassic; 3 | 4 | // Servicebook iterator to get operational tests for a given project 5 | Object getProjectTests(String name) { 6 | def projectName = name.toLowerCase(); 7 | def resp = httpRequest "https://servicebook-api.stage.mozaws.net/api/project"; 8 | def jsonSlurper = new JsonSlurperClassic(); 9 | def projects = jsonSlurper.parseText(resp.content); 10 | 11 | for (project in projects.data) { 12 | if (project.name.toLowerCase() == projectName) { 13 | echo "The project " + name + " was found!" 14 | 15 | def jenkin_tests = []; 16 | for (test in project.tests) { 17 | if (test.jenkins_pipeline) { 18 | echo "Adding one test " + test.name 19 | jenkin_tests << test; 20 | } 21 | } 22 | return jenkin_tests; 23 | } 24 | } 25 | echo "The project " + name + " was not found" 26 | return null; 27 | } 28 | 29 | 30 | 31 | def validURL(url) { 32 | allowedOrgs = ['Kinto', 'mozilla', 'mozilla-services'] 33 | 34 | for (allowedOrg in allowedOrgs) { 35 | if (url.startsWith('https://github.com/' + allowedOrg + '/')) { 36 | return true 37 | } 38 | } 39 | echo url + " is not a valid url" 40 | return false 41 | } 42 | 43 | 44 | def runStage(test) { 45 | stage(test.name) { 46 | if (validURL(test.url)) { 47 | echo "checking out " + test.url + ".git" 48 | node { 49 | checkout([ 50 | $class: 'GitSCM', 51 | branches: [[name: '*/master']], 52 | doGenerateSubmoduleConfigurations: false, 53 | extensions: [[$class: 'CleanCheckout']], 54 | submoduleCfg: [], 55 | userRemoteConfigs: [[url: test.url + '.git']] 56 | ]) 57 | } 58 | echo "checked out" 59 | node { 60 | sh "chmod +x run" 61 | sh "${WORKSPACE}/run" 62 | } 63 | } else { 64 | throw new IOException(test.url + " is not allowed") 65 | } 66 | } 67 | } 68 | 69 | def testProject(String name) { 70 | echo "Testing project " + name 71 | def failures = [] 72 | def tests = getProjectTests(name) 73 | 74 | for (test in tests) { 75 | try { 76 | echo "Running " + test 77 | echo "URL is " + test.url 78 | runStage(test) 79 | } catch (exc) { 80 | echo test.name + " failed" 81 | echo "Caught: ${exc}" 82 | failures.add(test.name) 83 | } 84 | } 85 | stage('Ship it!') { 86 | node { 87 | if (failures.size == 0) { 88 | sh 'exit 0' 89 | } else { 90 | sh 'exit 1' 91 | } 92 | } 93 | } 94 | } 95 | 96 | 97 | return this; 98 | -------------------------------------------------------------------------------- /vars/submitToTreeherder.groovy: -------------------------------------------------------------------------------- 1 | import groovy.json.JsonOutput 2 | import java.text.SimpleDateFormat 3 | 4 | /** Submit build results to Treeherder 5 | * 6 | * @param project project to submit results for 7 | * @param jobSymbol symbol for the job 8 | * @param jobName name for the job 9 | * @param artifactPath path for artifact(s) to publish 10 | * @param logPath path for log(s) to publish 11 | * @param groupSymbol symbol for the job group 12 | * @param groupName name for the job group 13 | */ 14 | def call(String project, 15 | String jobSymbol, 16 | String jobName, 17 | String artifactPath = null, 18 | String logPath = null, 19 | String groupSymbol = null, 20 | String groupName = null) { 21 | machine = getMachine() 22 | payload = [ 23 | taskId: UUID.randomUUID().toString(), 24 | buildSystem: machine['name'], 25 | origin: [kind: 'github.com', project: project, revision: "${GIT_COMMIT}"], 26 | display: getDisplay(jobSymbol, jobName, groupSymbol, groupName), 27 | state: 'completed', 28 | result: getResult(), 29 | jobKind: 'test', 30 | timeScheduled: getDateTime(currentBuild.timeInMillis), 31 | timeStarted: getDateTime(currentBuild.startTimeInMillis), 32 | timeCompleted: getDateTime(currentBuild.startTimeInMillis + currentBuild.duration), 33 | reason: 'scheduled', // TODO build cause: currentBuild.rawBuild.getCause().getShortDescription() 34 | productName: project, 35 | buildMachine: machine, 36 | runMachine: machine, 37 | jobInfo: [links: getJobLinks(artifactPath)], 38 | logs: getLogs(logPath), 39 | version: 1 40 | ] 41 | 42 | // TODO include ec2-metadata output in payload 43 | exchange = "exchange/${PULSE_USR}/jobs" 44 | routingKey = "${PULSE_USR}.${payload.productName}" 45 | 46 | // Skipping schema validation due to https://github.com/mozilla/fxtest-jenkins-pipeline/issues/22 47 | // schema = libraryResource 'org/mozilla/fxtest/pulse/schemas/treeherder.yml' 48 | // publishToPulse(exchange, routingKey, JsonOutput.toJson(payload), schema) 49 | 50 | publishToPulse(exchange, routingKey, JsonOutput.toJson(payload)) 51 | treeherderURL = "https://treeherder.mozilla.org/#/jobs?repo=${payload.productName}&revision=${payload.origin.revision}" 52 | echo "Results will be available to view at $treeherderURL" 53 | } 54 | 55 | def getMachine() { 56 | os = System.getProperty("os.name").toLowerCase().replaceAll('\\W', '-') 57 | version = System.getProperty("os.version").toLowerCase().replaceAll('\\W', '-') 58 | architecture = System.getProperty("os.arch") 59 | return [ 60 | name: new URI(env.JENKINS_URL).getHost().split('\\.')[0], 61 | platform: [os, version, architecture].join('-'), 62 | os: os, 63 | architecture: architecture 64 | ] 65 | } 66 | 67 | def getDisplay(jobSymbol, jobName, groupSymbol = null, groupName = null) { 68 | display = [ 69 | jobSymbol: jobSymbol, 70 | jobName: jobName, 71 | groupSymbol: groupSymbol ? groupSymbol : 'j' 72 | ] 73 | if ( groupName != null ) { 74 | display.groupName = groupName 75 | } else if ( groupSymbol != '?' ) { 76 | display.groupName = 'Executed by Jenkins' 77 | } 78 | return display 79 | } 80 | 81 | def getResult() { 82 | switch(currentBuild.result) { 83 | case 'FAILURE': 84 | case 'UNSTABLE': 85 | return 'fail' 86 | case 'SUCCESS': 87 | case null: 88 | return 'success' 89 | default: 90 | return 'unknown' 91 | } 92 | } 93 | 94 | def getDateTime(timeInMillis) { 95 | time = new Date(timeInMillis) 96 | return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX").format(time) 97 | } 98 | 99 | def getJobLinks(artifactPath) { 100 | links = [[url: env.BUILD_URL, linkText: env.BUILD_TAG, label: 'build']] 101 | if ( artifactPath != null ) { 102 | artifactLinks = publishToS3(artifactPath, 'net-mozaws-stage-fx-test-treeherder') 103 | for (link in artifactLinks) { 104 | links.add([url: link.url, linkText: link.name, label: 'artifact uploaded']) 105 | } 106 | } 107 | return links 108 | } 109 | 110 | def getLogs(logPath) { 111 | links = [] 112 | if ( logPath != null ) { 113 | logLinks = publishToS3(logPath, 'net-mozaws-stage-fx-test-treeherder') 114 | for (link in logLinks) { 115 | name = link.name =~ 'tbpl' ? 'buildbot_text' : link.name 116 | links.add([url: link.url, name: name]) 117 | } 118 | } 119 | return links 120 | } 121 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Shared libraries for Jenkins Pipeline 2 | This repository holds 3 | [shared libraries](https://jenkins.io/doc/book/pipeline/shared-libraries/) for 4 | [Jenkins pipelines](https://jenkins.io/doc/book/pipeline/) used by 5 | [Firefox Test Engineering](https://wiki.mozilla.org/TestEngineering). 6 | 7 | [![Build Status](https://travis-ci.org/mozilla/fxtest-jenkins-pipeline.svg?branch=master)](https://travis-ci.org/mozilla/fxtest-jenkins-pipeline) 8 | 9 | # Pipeline Steps 10 | ## ircNotification 11 | Sends a notification to IRC with the specified `channel`, `nick`, and `server`. 12 | By default it will connect to `irc.mozilla.org:6697` as `fxtest` and join the 13 | `#fx-test-alerts` channel. 14 | 15 | ### Examples 16 | ```groovy 17 | // use defaults 18 | ircNotification() 19 | 20 | // specify a channel 21 | ircNotification('#fx-test-alerts') 22 | 23 | // specify all values 24 | ircNotification( 25 | channel: '#fx-test-alerts', 26 | nick: 'fxtest', 27 | server: 'irc.mozilla.org:6697' 28 | ) 29 | ``` 30 | 31 | ## publishToPulse 32 | Publishes a `message`, to [Pulse] with the specified `exchange` and 33 | `routingKey`. If a schema is provided then it will be used to check that the 34 | message is valid. If the message fails to pass validation, details will be 35 | output to the console log and `ProcessingException` will be thrown. 36 | 37 | ### Requirements 38 | * Pulse credentials to be configured in Jenkins. 39 | * [Pipeline Model Definition Plugin] v1.2 or later. 40 | * [Pipeline Utility Steps Plugin]. 41 | 42 | ### Examples 43 | ```groovy 44 | // configure environment variables from credentials 45 | environment { 46 | PULSE = credentials('PULSE') 47 | } 48 | 49 | // send message without schema validation 50 | publishToPulse( 51 | exchange: "exchange/${PULSE_USR}", 52 | routingKey: "${PULSE_USR}.foo", 53 | message: 'foo' 54 | ) 55 | 56 | // send message with schema validation from resources 57 | schema = libraryResource 'org/mozilla/fxtest/pulse/schemas/treeherder.json' 58 | publishToPulse( 59 | exchange: "exchange/${PULSE_USR}", 60 | routingKey: "${PULSE_USR}.foo", 61 | message: 'foo', 62 | schema: schema 63 | ) 64 | ``` 65 | 66 | ## publishToS3 67 | Publishes the files at `path` to the specified Amazon S3 `bucket` and `region`. 68 | Defaults to region `us-east-1`. 69 | 70 | ### Requirements 71 | * [Amazon S3] bucket with appropriate permissions. 72 | * [S3 Plugin] with profile configured in Jenkins. 73 | 74 | ### Examples 75 | ```groovy 76 | // single file with default bucket and region 77 | publishToS3('results.html') 78 | 79 | // multiple files with specified bucket and region 80 | publishToS3( 81 | path: 'results/*', 82 | bucket: 'foo', 83 | region: 'bar' 84 | ) 85 | ``` 86 | 87 | ## submitToActiveData 88 | Publishes the structured log(s) at `logPath` to ActiveData. 89 | 90 | ### Requirements 91 | See [publishToS3](#publishtos3). 92 | 93 | ### Examples 94 | ```groovy 95 | submitToActiveData('results/raw.txt') 96 | ``` 97 | 98 | ## submitToTreeherder 99 | Submits the build result for `project` to [Treeherder] using the specified 100 | `jobSymbol` and `jobName`. If provided, files located by `artifactPath` and 101 | `logPath` will be published to Amazon S3 and linked from the build results. By 102 | default the job will have a `groupSymbol` of 'j', and a `groupName` of 103 | 'Executed by Jenkins'. To leave the job ungrouped pass a `groupSymbol` of '?'. 104 | 105 | ### Requirements 106 | See [publishToS3](#publishtos3) and [publishToPulse](#publishtopulse). 107 | 108 | ### Examples 109 | ```groovy 110 | // submit default grouped build results without artifacts or logs 111 | submitToTreeherder( 112 | project: 'foo', 113 | jobSymbol: 'T', 114 | jobName: 'Tests' 115 | ) 116 | 117 | // submit ungrouped build results without artifacts or logs 118 | submitToTreeherder( 119 | project: 'foo', 120 | jobSymbol: 'T', 121 | jobName: 'Tests', 122 | groupSymbol: '?' 123 | ) 124 | 125 | // submit custom grouped build results with artifacts and log 126 | submitToTreeherder( 127 | project: 'foo', 128 | jobSymbol: 'I', 129 | jobName: 'Integration tests', 130 | artifactPath: 'results/*', 131 | logPath: 'results/tbpl.txt', 132 | groupSymbol: 'T', 133 | groupName: 'Tests' 134 | ) 135 | ``` 136 | 137 | ## writeCapabilities 138 | Writes a JSON file containing the items from the `capabilities` map to the 139 | specified `path` (for use by [pytest-selenium]). If omitted, the `path` 140 | defaults to `capabilities.json` in the working directory. 141 | 142 | ### Examples 143 | ```groovy 144 | capabilities = [ 145 | browserName: 'Firefox', 146 | version: '51.0', 147 | platform: 'Windows 10' 148 | ] 149 | 150 | // write capabilities to default path 151 | writeCapabilities(capabilities) 152 | 153 | // write capabilities to specified path 154 | writeCapabilities( 155 | desiredCapabilities: capabilities, 156 | path: 'fx51win10.json' 157 | ) 158 | ``` 159 | # ServiceBook 160 | ## testProject 161 | Queries the Service Book project API for the given project `name`, iterates over 162 | its associated test repositories, checks them out from SCM, and executes their 163 | ```run``` file(s). Finally, it returns ```exit 0``` on successful/passing 164 | tests, and ```exit 1``` in the event of failed builds. 165 | 166 | ## Examples 167 | ```groovy 168 | @Library('fxtest') _ 169 | 170 | def sb = new org.mozilla.fxtest.ServiceBook() 171 | sb.testProject('kinto') 172 | ``` 173 | 174 | ## Requirements 175 | * [HTTP Request Plugin] v1.8.20 or later. 176 | 177 | # How to run tests 178 | 179 | Make sure you have the latest Gradle install and run: 180 | 181 | ```sh 182 | $ gradle check 183 | BUILD SUCCESSFUL in 1s 184 | 2 actionable tasks: 2 up-to-date 185 | ``` 186 | 187 | # Version History 188 | 189 | ### 1.10 (2018-01-15) 190 | * Remove superfluous test stage when using Service Book. 191 | * Use `GIT_COMMIT` when reporting revision to Treeherder. 192 | 193 | ### 1.9 (2017-09-14) 194 | * Fixed the production Service Book API URL 195 | 196 | ### 1.8 (2017-09-14) 197 | * Greatly simplified Service Book API calls using the HTTP Request Plugin. 198 | 199 | ### 1.7 (2017-09-04) 200 | * Introduced ```ServiceBook``` class, with ```testProject``` method to execute tests for all pipeline associated with the specified project name. 201 | 202 | ### 1.6 (2017-04-13) 203 | * Changed TBPL log name to `buildbot_text` in Treeherder message for log parsing. ([#12](https://github.com/mozilla/fxtest-jenkins-pipeline/issues/12)) 204 | * Switched to YAML schema for Treeherder message validation. ([#2](https://github.com/mozilla/fxtest-jenkins-pipeline/issues/2)) 205 | * Added link to Treeherder results to console log. ([#11](https://github.com/mozilla/fxtest-jenkins-pipeline/issues/11)) 206 | * Provided a default group for Treeherder jobs. ([#13](https://github.com/mozilla/fxtest-jenkins-pipeline/issues/13)) 207 | 208 | ### 1.5 (2017-03-31) 209 | * Changed S3 profile to `fx-test-jenkins-s3-publisher`. 210 | 211 | ### 1.4 (2017-02-31) 212 | * Introduced `publishToS3`, `publishToPulse`, and `submitToTreeherder` steps. 213 | 214 | ### 1.3 (2017-02-23) 215 | * Don't mark jobs as unstable if `submitToActiveData` fails. 216 | 217 | ### 1.2 (2017-02-22) 218 | * Added `submitToActiveData` step for publishing structured logs to S3 for processing by ActiveData. 219 | 220 | ### 1.1 (2017-02-13) 221 | * Changed order of arguments for `ircNotification` for ease of specifying alternate channel. 222 | 223 | ### 1.0 (2017-02-13) 224 | * Initial release with `ircNotification` and `writeCapabilities` steps. 225 | 226 | [Pulse]: https://wiki.mozilla.org/Auto-tools/Projects/Pulse 227 | [Pipeline Utility Steps Plugin]: https://wiki.jenkins-ci.org/display/JENKINS/Pipeline+Utility+Steps+Plugin 228 | [S3 Plugin]: https://wiki.jenkins-ci.org/display/JENKINS/S3+Plugin 229 | [Amazon S3]: https://aws.amazon.com/s3/ 230 | [Treeherder]: https://wiki.mozilla.org/Auto-tools/Projects/Treeherder 231 | [pytest-selenium]: http://pytest-selenium.readthedocs.io/en/latest/user_guide.html#capabilities-files 232 | [Pipeline Model Definition Plugin]: https://wiki.jenkins-ci.org/display/JENKINS/Pipeline+Model+Definition+Plugin 233 | [HTTP Request Plugin]: https://wiki.jenkins.io/display/JENKINS/HTTP+Request+Plugin 234 | -------------------------------------------------------------------------------- /resources/org/mozilla/fxtest/pulse/schemas/treeherder.yml: -------------------------------------------------------------------------------- 1 | $schema: "http://json-schema.org/draft-04/schema#" 2 | title: "Job Definition" 3 | description: | 4 | Definition of a single job that can be added to Treeherder 5 | Project is determined by the routing key, so we don't need to specify it here. 6 | type: "object" 7 | properties: 8 | taskId: 9 | title: "taskId" 10 | description: | 11 | This could just be what was formerly submitted as a job_guid in the 12 | REST API. 13 | type: "string" 14 | pattern: "^[A-Za-z0-9/+-]+$" 15 | minLength: 1 16 | maxLength: 50 17 | retryId: 18 | title: "retryId" 19 | description: | 20 | The infrastructure retry iteration on this job. The number of times this 21 | job has been retried by the infrastructure. 22 | If it's the 1st time running, then it should be 0. If this is the first 23 | retry, it will be 1, etc. 24 | type: "integer" 25 | default: 0 26 | minimum: 0 27 | 28 | isRetried: 29 | description: True indicates this job has been retried. 30 | type: "boolean" 31 | 32 | buildSystem: 33 | description: | 34 | The name of the build system that initiated this content. Some examples 35 | are "buildbot" and "taskcluster". But this could be any name. This 36 | value will be used in the routing key for retriggering jobs in the 37 | publish-job-action task. 38 | type: "string" 39 | pattern: "^[\\w-]+$" 40 | minLength: 1 41 | maxLength: 25 42 | 43 | origin: 44 | oneOf: 45 | - type: "object" 46 | description: | 47 | PREFERRED: An HG job that only has a revision. This is for all 48 | jobs going forward. 49 | properties: 50 | kind: 51 | type: "string" 52 | enum: ['hg.mozilla.org'] 53 | project: 54 | type: "string" 55 | pattern: "^[\\w-]+$" 56 | minLength: 1 57 | maxLength: 50 58 | revision: 59 | type: "string" 60 | pattern: "^[0-9a-f]+$" 61 | minLength: 40 62 | maxLength: 40 63 | pushLogID: 64 | type: "integer" 65 | required: [kind, project, revision] 66 | 67 | - type: "object" 68 | description: | 69 | BACKWARD COMPATABILITY: An HG job that only has a revision_hash. 70 | Some repos like mozilla-beta have not yet merged in the code that 71 | allows them access to the revision. 72 | properties: 73 | kind: 74 | type: "string" 75 | enum: ['hg.mozilla.org'] 76 | project: 77 | type: "string" 78 | pattern: "^[\\w-]+$" 79 | minLength: 1 80 | maxLength: 50 81 | revision_hash: 82 | type: "string" 83 | pattern: "^[0-9a-f]+$" 84 | minLength: 40 85 | maxLength: 40 86 | pushLogID: 87 | type: "integer" 88 | required: [kind, project, revision_hash] 89 | 90 | - type: "object" 91 | properties: 92 | kind: 93 | type: "string" 94 | enum: ['github.com'] 95 | owner: 96 | description: | 97 | This could be the organization or the individual git username 98 | depending on who owns the repo. 99 | type: "string" 100 | pattern: "^[\\w-]+$" 101 | minLength: 1 102 | maxLength: 50 103 | project: 104 | type: "string" 105 | pattern: "^[\\w-]+$" 106 | minLength: 1 107 | maxLength: 50 108 | revision: 109 | type: "string" 110 | minLength: 40 111 | maxLength: 40 112 | pullRequestID: 113 | type: "integer" 114 | required: [kind, project, revision] 115 | 116 | display: 117 | type: "object" 118 | properties: 119 | jobSymbol: 120 | title: "jobSymbol" 121 | type: "string" 122 | minLength: 0 123 | maxLength: 25 124 | chunkId: 125 | title: "chunkId" 126 | type: "integer" 127 | minimum: 1 128 | chunkCount: 129 | title: "chunkCount" 130 | type: "integer" 131 | minimum: 1 132 | groupSymbol: 133 | title: "group symbol" 134 | type: "string" 135 | minLength: 1 136 | maxLength: 25 137 | jobName: 138 | title: "job name" 139 | type: "string" 140 | minLength: 1 141 | maxLength: 100 142 | groupName: 143 | title: "group name" 144 | type: "string" 145 | minLength: 1 146 | maxLength: 100 147 | required: 148 | - jobName 149 | - jobSymbol 150 | - groupSymbol 151 | 152 | 153 | state: 154 | title: "state" 155 | description: | 156 | unscheduled: not yet scheduled 157 | pending: not yet started 158 | running: currently in progress 159 | completed: Job ran through to completion 160 | type: "string" 161 | enum: 162 | - unscheduled 163 | - pending 164 | - running 165 | - completed 166 | result: 167 | title: "result" 168 | description: | 169 | fail: A failure 170 | exception: An infrastructure error/exception 171 | success: Build/Test executed without error or failure 172 | canceled: The job was cancelled by a user 173 | unknown: When the job is not yet completed 174 | type: "string" 175 | enum: 176 | - success 177 | - fail 178 | - exception 179 | - canceled 180 | - unknown 181 | jobKind: 182 | type: "string" 183 | default: "other" 184 | enum: 185 | - build 186 | - test 187 | - other 188 | tier: 189 | type: "integer" 190 | minimum: 1 191 | maximum: 3 192 | 193 | coalesced: 194 | description: The job guids that were coalesced to this job. 195 | title: "coalesced" 196 | type: "array" 197 | items: 198 | title: "job guid" 199 | type: "string" 200 | pattern: "^[\\w/+-]+$" 201 | minLength: 1 202 | maxLength: 50 203 | 204 | 205 | # time data 206 | timeScheduled: 207 | type: "string" 208 | format: "date-time" 209 | timeStarted: 210 | type: "string" 211 | format: "date-time" 212 | timeCompleted: 213 | type: "string" 214 | format: "date-time" 215 | 216 | labels: 217 | title: "labels" 218 | description: | 219 | Labels are a dimension of a platform. The values here can vary wildly, 220 | so most strings are valid for this. The list of labels that are used 221 | is maleable going forward. 222 | 223 | These were formerly known as "Options" within "Option Collections" but 224 | calling labels now so they can be understood to be just strings that 225 | denotes a characteristic of the job. 226 | 227 | Some examples of labels that have been used: 228 | opt Optimize Compiler GCC optimize flags 229 | debug Debug flags passed in 230 | pgo Profile Guided Optimization - Like opt, but runs with profiling, then builds again using that profiling 231 | asan Address Sanitizer 232 | tsan Thread Sanitizer Build 233 | type: "array" 234 | items: 235 | type: "string" 236 | minLength: 1 237 | maxLength: 50 238 | pattern: "^[\\w-]+$" 239 | 240 | owner: 241 | description: | 242 | Description of who submitted the job: gaia | scheduler name | username | email 243 | title: "owner" 244 | type: "string" 245 | minLength: 1 246 | maxLength: 50 247 | reason: 248 | description: | 249 | Examples include: 250 | - scheduled 251 | - scheduler 252 | - Self-serve: Rebuilt by foo@example.com 253 | - Self-serve: Requested by foo@example.com 254 | - The Nightly scheduler named 'b2g_mozilla-inbound periodic' triggered this build 255 | - unknown 256 | type: "string" 257 | minLength: 1 258 | maxLength: 125 259 | productName: 260 | description: | 261 | Examples include: 262 | - 'b2g' 263 | - 'firefox' 264 | - 'taskcluster' 265 | - 'xulrunner' 266 | type: "string" 267 | minLength: 1 268 | maxLength: 125 269 | 270 | buildMachine: 271 | $ref: "#/definitions/machine" 272 | runMachine: 273 | $ref: "#/definitions/machine" 274 | 275 | jobInfo: 276 | description: | 277 | Definition of the Job Info for a job. These are extra data 278 | fields that go along with a job that will be displayed in 279 | the details panel within Treeherder. 280 | id: "jobInfo" 281 | type: object 282 | properties: 283 | summary: 284 | type: string 285 | description: | 286 | Plain text description of the job and its state. Submitted with 287 | the final message about a task. 288 | links: 289 | type: array 290 | items: 291 | - type: object 292 | description: | 293 | List of URLs shown as key/value pairs. Shown as: 294 | "