const _ = require('lodash') const Promise = require('bluebird') const bumpercar = require('@cypress/bumpercar') const path = require('path') const la = require('lazy-ass') const check = require('check-more-types') const R = require('ramda') const { configFromEnvOrJsonFile, filenameToShellVariable } = require('@cypress/env-or-json-file') const makeEmptyGithubCommit = require('make-empty-github-commit') const parse = require('parse-github-repo-url') const { setCommitStatus } = require('@cypress/github-commit-status-check') let car = null // all the projects to trigger / run / change environment variables for const _PROVIDERS = { appVeyor: { main: 'cypress-io/cypress', win32: [ 'cypress-io/cypress-test-tiny', 'cypress-io/cypress-test-example-repos', ], }, circle: { main: 'cypress-io/cypress', linux: [ 'cypress-io/cypress-test-tiny', 'cypress-io/cypress-test-module-api', 'cypress-io/cypress-test-node-versions', 'cypress-io/cypress-test-nested-projects', 'cypress-io/cypress-test-ci-environments', 'cypress-io/cypress-test-example-repos', ], darwin: [ 'cypress-io/cypress-test-tiny', 'cypress-io/cypress-test-example-repos', ], }, } const remapProjects = function (projectsByProvider) { const list = [] _.mapValues(projectsByProvider, (provider, name) => { const remapPlatform = (platform, repos) => { return repos.forEach((repo) => { return list.push({ repo, provider: name, platform, }) }) } if (provider.win32) { remapPlatform('win32', provider.win32) } if (provider.linux) { remapPlatform('linux', provider.linux) } if (provider.darwin) { return remapPlatform('darwin', provider.darwin) } }) return list } // make flat list of objects // {repo, provider, platform} const PROJECTS = remapProjects(_PROVIDERS) const getCiConfig = function () { const key = path.join('scripts', 'support', 'ci.json') const config = configFromEnvOrJsonFile(key) if (!config) { console.error('⛔️ Cannot find CI credentials') console.error('Using @cypress/env-or-json-file module') console.error('and filename', key) console.error('which is environment variable', filenameToShellVariable(key)) throw new Error('CI config not found') } return config } const awaitEachProjectAndProvider = function (projects, fn, filter = R.identity) { const creds = getCiConfig() // configure a new Bumpercar const providers = {} if (check.unemptyString(creds.githubToken)) { providers.travis = { githubToken: process.env.GH_TOKEN, } } if (check.unemptyString(creds.circleToken)) { providers.circle = { circleToken: creds.circleToken, } } if (check.unemptyString(creds.appVeyorToken)) { providers.appVeyor = { appVeyorToken: creds.appVeyorToken, } } const providerNames = Object.keys(providers) console.log('configured providers', providerNames) la(check.not.empty(providerNames), 'empty list of providers') car = bumpercar.create({ providers }) const filteredProjects = R.filter(filter, projects) if (check.empty(filteredProjects)) { console.log('⚠️ zero filtered projects left after filtering') } console.log('filtered projects:') console.table(filteredProjects) return Promise.mapSeries(filteredProjects, (project) => { return fn(project.repo, project.provider, creds) }) } // do not trigger all projects if there is specific provider // for example appVeyor should be used for Windows testing const getFilterByProvider = function (providerName, platformName) { let platformFilter; let providerFilter if (providerName) { console.log('only allow projects for provider', providerName) providerFilter = R.propEq('provider', providerName) } else { providerFilter = R.identity } if (platformName) { console.log('only allow projects for platform', platformName) platformFilter = R.propEq('platform', platformName) } else { platformFilter = R.identity } // combined filter is when both filters pass const projectFilter = R.allPass([providerFilter, platformFilter]) return projectFilter } module.exports = { _PROVIDERS, remapProjects, getFilterByProvider, // in each project, set a couple of environment variables version (nameOrUrl, binaryVersionOrUrl, platform, providerName) { console.log('All possible projects:') console.table(PROJECTS) la(check.unemptyString(nameOrUrl), 'missing cypress name or url to set', nameOrUrl) if (check.semver(nameOrUrl)) { console.log('for version', nameOrUrl) nameOrUrl = `cypress@${nameOrUrl}` console.log('full NPM install name is', nameOrUrl) } la(check.unemptyString(binaryVersionOrUrl), 'missing binary version or url', binaryVersionOrUrl) const result = { versionName: nameOrUrl, binary: binaryVersionOrUrl, } const projectFilter = getFilterByProvider(providerName) const updateProject = function (project, provider) { console.log('setting environment variables in', project) return car.updateProjectEnv(project, provider, { CYPRESS_NPM_PACKAGE_NAME: nameOrUrl, CYPRESS_INSTALL_BINARY: binaryVersionOrUrl, }) } return awaitEachProjectAndProvider(PROJECTS, updateProject, projectFilter) .then(R.always(result)) }, // triggers test projects on multiple CIs // the test projects will exercise the new version of // the Cypress test runner we just built runTestProjects (getStatusAndMessage, providerName, version, platform) { const projectFilter = getFilterByProvider(providerName, platform) const makeCommit = function (project, provider, creds) { // make empty commit to trigger CIs // project is owner/repo string like cypress-io/cypress-test-tiny console.log('making commit to project', project) // print if we have a few github variables present console.log('do we have GH_APP_ID?', Boolean(process.env.GH_APP_ID)) console.log('do we have GH_INSTALLATION_ID?', Boolean(process.env.GH_INSTALLATION_ID)) console.log('do we have GH_PRIVATE_KEY?', Boolean(process.env.GH_PRIVATE_KEY)) console.log('do we have GH_TOKEN?', Boolean(process.env.GH_TOKEN)) const parsedRepo = parse(project) const owner = parsedRepo[0] const repo = parsedRepo[1] let { status, message } = getStatusAndMessage(repo) if (!message) { message = `\ Testing new Cypress version ${version} \ ` if (process.env.CIRCLE_BUILD_URL) { message += '\n' message += `Circle CI build url ${process.env.CIRCLE_BUILD_URL}` } if (process.env.APPVEYOR) { const slug = process.env.APPVEYOR_PROJECT_SLUG const build = process.env.APPVEYOR_BUILD_ID message += '\n' message += `AppVeyor CI ${slug} ${build}` } } const defaultOptions = { owner, repo, message, token: process.env.GH_TOKEN, } const createGithubCommitStatusCheck = function ({ sha }) { if (!status) { return } // status is {owner, repo, sha} and maybe a few other properties const isStatus = check.schema({ owner: check.unemptyString, repo: check.unemptyString, sha: check.commitId, context: check.unemptyString, platform: check.unemptyString, arch: check.unemptyString, }) if (!isStatus(status)) { console.error('Invalid status object %o', status) } const targetUrl = `https://github.com/${owner}/${repo}/commit/${sha}` const commitStatusOptions = { targetUrl, owner: status.owner, repo: status.repo, sha: status.sha, context: status.context, state: 'pending', description: `${owner}/${repo}`, } console.log( 'creating commit status check', commitStatusOptions.description, commitStatusOptions.context, ) return setCommitStatus(commitStatusOptions) } if (!version) { return makeEmptyGithubCommit(defaultOptions).then(createGithubCommitStatusCheck) } // first try to commit to branch for next upcoming version const specificBranchOptions = { owner, repo, token: creds.githubToken, message, branch: version, } return makeEmptyGithubCommit(specificBranchOptions) .catch(() => { // maybe there is no branch for next version // try default branch return makeEmptyGithubCommit(defaultOptions) }).then(createGithubCommitStatusCheck) } return awaitEachProjectAndProvider(PROJECTS, makeCommit, projectFilter) }, }