@cypress/github-action 2.11.8 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/action.yml +1 -1
- package/dist/index.js +866 -866
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -5791,792 +5791,792 @@ exports.issueCommand = issueCommand;
|
|
|
5791
5791
|
/* 104 */
|
|
5792
5792
|
/***/ (function(__unusedmodule, __unusedexports, __webpack_require__) {
|
|
5793
5793
|
|
|
5794
|
-
// @ts-check
|
|
5795
|
-
const { restoreCache, saveCache } = __webpack_require__(692)
|
|
5796
|
-
const core = __webpack_require__(470)
|
|
5797
|
-
const exec = __webpack_require__(986)
|
|
5798
|
-
const io = __webpack_require__(1)
|
|
5799
|
-
const { Octokit } = __webpack_require__(448)
|
|
5800
|
-
const hasha = __webpack_require__(309)
|
|
5801
|
-
const fs = __webpack_require__(747)
|
|
5802
|
-
const os = __webpack_require__(87)
|
|
5803
|
-
const path = __webpack_require__(622)
|
|
5804
|
-
const quote = __webpack_require__(531)
|
|
5805
|
-
const cliParser = __webpack_require__(324)()
|
|
5806
|
-
const findYarnWorkspaceRoot = __webpack_require__(638)
|
|
5807
|
-
const debug = __webpack_require__(784)('@cypress/github-action')
|
|
5808
|
-
const { ping } = __webpack_require__(946)
|
|
5809
|
-
|
|
5810
|
-
/**
|
|
5811
|
-
* Parses input command, finds the tool and
|
|
5812
|
-
* the runs the command.
|
|
5813
|
-
*/
|
|
5814
|
-
const execCommand = (
|
|
5815
|
-
fullCommand,
|
|
5816
|
-
waitToFinish = true,
|
|
5817
|
-
label = 'executing'
|
|
5818
|
-
) => {
|
|
5819
|
-
const cwd = cypressCommandOptions.cwd
|
|
5820
|
-
|
|
5821
|
-
console.log('%s command "%s"', label, fullCommand)
|
|
5822
|
-
console.log('current working directory "%s"', cwd)
|
|
5823
|
-
|
|
5824
|
-
const args = cliParser.parse(fullCommand)
|
|
5825
|
-
debug(`parsed command: ${args.join(' ')}`)
|
|
5826
|
-
|
|
5827
|
-
return io.which(args[0], true).then((toolPath) => {
|
|
5828
|
-
debug(`found command "${toolPath}"`)
|
|
5829
|
-
debug(`with arguments ${args.slice(1).join(' ')}`)
|
|
5830
|
-
|
|
5831
|
-
const toolArguments = args.slice(1)
|
|
5832
|
-
const argsString = toolArguments.join(' ')
|
|
5833
|
-
debug(`running ${quote(toolPath)} ${argsString} in ${cwd}`)
|
|
5834
|
-
debug(`waiting for the command to finish? ${waitToFinish}`)
|
|
5835
|
-
|
|
5836
|
-
const promise = exec.exec(
|
|
5837
|
-
quote(toolPath),
|
|
5838
|
-
toolArguments,
|
|
5839
|
-
cypressCommandOptions
|
|
5840
|
-
)
|
|
5841
|
-
if (waitToFinish) {
|
|
5842
|
-
return promise
|
|
5843
|
-
}
|
|
5844
|
-
})
|
|
5845
|
-
}
|
|
5846
|
-
|
|
5847
|
-
const isWindows = () => os.platform() === 'win32'
|
|
5848
|
-
const isUrl = (s) => /^https?:\/\//.test(s)
|
|
5849
|
-
|
|
5850
|
-
/**
|
|
5851
|
-
* Quote strings that contain Windows delimiters.
|
|
5852
|
-
* https://github.com/cypress-io/github-action/issues/459
|
|
5853
|
-
*/
|
|
5854
|
-
const quoteWindowsArgument = (value) => {
|
|
5855
|
-
if (!/[,;=\s]/.test(value)) {
|
|
5856
|
-
return value
|
|
5857
|
-
}
|
|
5858
|
-
return quote(value)
|
|
5859
|
-
}
|
|
5860
|
-
|
|
5861
|
-
/**
|
|
5862
|
-
* Returns true if the Cypress binary installation was skipped
|
|
5863
|
-
* via an environment variable https://on.cypress.io/installing
|
|
5864
|
-
*/
|
|
5865
|
-
const isCypressBinarySkipped = () =>
|
|
5866
|
-
process.env.CYPRESS_INSTALL_BINARY === '0'
|
|
5867
|
-
|
|
5868
|
-
const homeDirectory = os.homedir()
|
|
5869
|
-
const platformAndArch = `${process.platform}-${process.arch}`
|
|
5870
|
-
|
|
5871
|
-
const startWorkingDirectory = process.cwd()
|
|
5872
|
-
// seems the working directory should be absolute to work correctly
|
|
5873
|
-
// https://github.com/cypress-io/github-action/issues/211
|
|
5874
|
-
const workingDirectory = core.getInput('working-directory')
|
|
5875
|
-
? path.resolve(core.getInput('working-directory'))
|
|
5876
|
-
: startWorkingDirectory
|
|
5877
|
-
debug(`working directory ${workingDirectory}`)
|
|
5878
|
-
|
|
5879
|
-
/**
|
|
5880
|
-
* When running "npm install" or any other Cypress-related commands,
|
|
5881
|
-
* use the install directory as current working directory
|
|
5882
|
-
*/
|
|
5883
|
-
const cypressCommandOptions = {
|
|
5884
|
-
cwd: workingDirectory
|
|
5885
|
-
}
|
|
5886
|
-
|
|
5887
|
-
const yarnFilename = path.join(
|
|
5888
|
-
findYarnWorkspaceRoot(workingDirectory) || workingDirectory,
|
|
5889
|
-
'yarn.lock'
|
|
5890
|
-
)
|
|
5891
|
-
const packageLockFilename = path.join(
|
|
5892
|
-
workingDirectory,
|
|
5893
|
-
'package-lock.json'
|
|
5894
|
-
)
|
|
5895
|
-
|
|
5896
|
-
const useYarn = () => fs.existsSync(yarnFilename)
|
|
5897
|
-
|
|
5898
|
-
const lockHash = () => {
|
|
5899
|
-
const lockFilename = useYarn() ? yarnFilename : packageLockFilename
|
|
5900
|
-
const fileHash = hasha.fromFileSync(lockFilename)
|
|
5901
|
-
debug(`Hash from file ${lockFilename} is ${fileHash}`)
|
|
5902
|
-
return fileHash
|
|
5903
|
-
}
|
|
5904
|
-
|
|
5905
|
-
// enforce the same NPM cache folder across different operating systems
|
|
5906
|
-
const NPM_CACHE_FOLDER = path.join(homeDirectory, '.npm')
|
|
5907
|
-
const getNpmCache = () => {
|
|
5908
|
-
const o = {}
|
|
5909
|
-
let key = core.getInput('cache-key')
|
|
5910
|
-
const hash = lockHash()
|
|
5911
|
-
if (!key) {
|
|
5912
|
-
if (useYarn()) {
|
|
5913
|
-
key = `yarn-${platformAndArch}-${hash}`
|
|
5914
|
-
} else {
|
|
5915
|
-
key = `npm-${platformAndArch}-${hash}`
|
|
5916
|
-
}
|
|
5917
|
-
} else {
|
|
5918
|
-
console.log('using custom cache key "%s"', key)
|
|
5919
|
-
}
|
|
5920
|
-
|
|
5921
|
-
if (useYarn()) {
|
|
5922
|
-
o.inputPath = path.join(homeDirectory, '.cache', 'yarn')
|
|
5923
|
-
} else {
|
|
5924
|
-
o.inputPath = NPM_CACHE_FOLDER
|
|
5925
|
-
}
|
|
5926
|
-
|
|
5927
|
-
// use exact restore key to prevent NPM cache from growing
|
|
5928
|
-
// https://glebbahmutov.com/blog/do-not-let-npm-cache-snowball/
|
|
5929
|
-
o.restoreKeys = o.primaryKey = key
|
|
5930
|
-
return o
|
|
5931
|
-
}
|
|
5932
|
-
|
|
5933
|
-
// custom Cypress binary cache folder
|
|
5934
|
-
// see https://on.cypress.io/caching
|
|
5935
|
-
const CYPRESS_CACHE_FOLDER =
|
|
5936
|
-
process.env.CYPRESS_CACHE_FOLDER ||
|
|
5937
|
-
path.join(homeDirectory, '.cache', 'Cypress')
|
|
5938
|
-
debug(`using custom Cypress cache folder "${CYPRESS_CACHE_FOLDER}"`)
|
|
5939
|
-
|
|
5940
|
-
const getCypressBinaryCache = () => {
|
|
5941
|
-
const o = {
|
|
5942
|
-
inputPath: CYPRESS_CACHE_FOLDER
|
|
5943
|
-
}
|
|
5944
|
-
const hash = lockHash()
|
|
5945
|
-
const key = `cypress-${platformAndArch}-${hash}`
|
|
5946
|
-
|
|
5947
|
-
// use only exact restore key to prevent cached folder growing in size
|
|
5948
|
-
// https://glebbahmutov.com/blog/do-not-let-cypress-cache-snowball/
|
|
5949
|
-
o.restoreKeys = o.primaryKey = key
|
|
5950
|
-
return o
|
|
5951
|
-
}
|
|
5952
|
-
|
|
5953
|
-
const restoreCachedNpm = () => {
|
|
5954
|
-
debug('trying to restore cached NPM modules')
|
|
5955
|
-
const NPM_CACHE = getNpmCache()
|
|
5956
|
-
return restoreCache([NPM_CACHE.inputPath], NPM_CACHE.primaryKey, [
|
|
5957
|
-
NPM_CACHE.restoreKeys
|
|
5958
|
-
]).catch((e) => {
|
|
5959
|
-
console.warn('Restoring NPM cache error: %s', e.message)
|
|
5960
|
-
})
|
|
5961
|
-
}
|
|
5962
|
-
|
|
5963
|
-
const saveCachedNpm = () => {
|
|
5964
|
-
debug('saving NPM modules')
|
|
5965
|
-
const NPM_CACHE = getNpmCache()
|
|
5966
|
-
return saveCache([NPM_CACHE.inputPath], NPM_CACHE.primaryKey).catch(
|
|
5967
|
-
(e) => {
|
|
5968
|
-
console.warn('Saving NPM cache error: %s', e.message)
|
|
5969
|
-
}
|
|
5970
|
-
)
|
|
5971
|
-
}
|
|
5972
|
-
|
|
5973
|
-
const restoreCachedCypressBinary = () => {
|
|
5974
|
-
debug('trying to restore cached Cypress binary')
|
|
5975
|
-
const CYPRESS_BINARY_CACHE = getCypressBinaryCache()
|
|
5976
|
-
return restoreCache(
|
|
5977
|
-
[CYPRESS_BINARY_CACHE.inputPath],
|
|
5978
|
-
CYPRESS_BINARY_CACHE.primaryKey,
|
|
5979
|
-
[CYPRESS_BINARY_CACHE.restoreKeys]
|
|
5980
|
-
).catch((e) => {
|
|
5981
|
-
console.warn('Restoring Cypress cache error: %s', e.message)
|
|
5982
|
-
})
|
|
5983
|
-
}
|
|
5984
|
-
|
|
5985
|
-
const saveCachedCypressBinary = () => {
|
|
5986
|
-
debug('saving Cypress binary')
|
|
5987
|
-
|
|
5988
|
-
if (isCypressBinarySkipped()) {
|
|
5989
|
-
debug('Skipping Cypress cache save, binary is not installed')
|
|
5990
|
-
return Promise.resolve()
|
|
5991
|
-
}
|
|
5992
|
-
|
|
5993
|
-
const CYPRESS_BINARY_CACHE = getCypressBinaryCache()
|
|
5994
|
-
return saveCache(
|
|
5995
|
-
[CYPRESS_BINARY_CACHE.inputPath],
|
|
5996
|
-
CYPRESS_BINARY_CACHE.primaryKey
|
|
5997
|
-
).catch((e) => {
|
|
5998
|
-
console.warn('Saving Cypress cache error: %s', e.message)
|
|
5999
|
-
})
|
|
6000
|
-
}
|
|
6001
|
-
|
|
6002
|
-
const install = () => {
|
|
6003
|
-
// prevent lots of progress messages during install
|
|
6004
|
-
core.exportVariable('CI', '1')
|
|
6005
|
-
core.exportVariable('CYPRESS_CACHE_FOLDER', CYPRESS_CACHE_FOLDER)
|
|
6006
|
-
// set NPM cache path in case the user has custom install command
|
|
6007
|
-
core.exportVariable('npm_config_cache', NPM_CACHE_FOLDER)
|
|
6008
|
-
|
|
6009
|
-
// Note: need to quote found tool to avoid Windows choking on
|
|
6010
|
-
// npm paths with spaces like "C:\Program Files\nodejs\npm.cmd ci"
|
|
6011
|
-
const installCommand = core.getInput('install-command')
|
|
6012
|
-
if (installCommand) {
|
|
6013
|
-
debug(`using custom install command "${installCommand}"`)
|
|
6014
|
-
return execCommand(installCommand, true, 'install command')
|
|
6015
|
-
}
|
|
6016
|
-
|
|
6017
|
-
if (useYarn()) {
|
|
6018
|
-
debug('installing NPM dependencies using Yarn')
|
|
6019
|
-
return io.which('yarn', true).then((yarnPath) => {
|
|
6020
|
-
debug(`yarn at "${yarnPath}"`)
|
|
6021
|
-
return exec.exec(
|
|
6022
|
-
quote(yarnPath),
|
|
6023
|
-
['--frozen-lockfile'],
|
|
6024
|
-
cypressCommandOptions
|
|
6025
|
-
)
|
|
6026
|
-
})
|
|
6027
|
-
} else {
|
|
6028
|
-
debug('installing NPM dependencies')
|
|
6029
|
-
|
|
6030
|
-
return io.which('npm', true).then((npmPath) => {
|
|
6031
|
-
debug(`npm at "${npmPath}"`)
|
|
6032
|
-
return exec.exec(quote(npmPath), ['ci'], cypressCommandOptions)
|
|
6033
|
-
})
|
|
6034
|
-
}
|
|
6035
|
-
}
|
|
6036
|
-
|
|
6037
|
-
const listCypressBinaries = () => {
|
|
6038
|
-
debug(
|
|
6039
|
-
`Cypress versions in the cache folder ${CYPRESS_CACHE_FOLDER}`
|
|
6040
|
-
)
|
|
6041
|
-
|
|
6042
|
-
if (isCypressBinarySkipped()) {
|
|
6043
|
-
debug('Skipping Cypress cache list, binary is not installed')
|
|
6044
|
-
return Promise.resolve()
|
|
6045
|
-
}
|
|
6046
|
-
|
|
6047
|
-
core.exportVariable('CYPRESS_CACHE_FOLDER', CYPRESS_CACHE_FOLDER)
|
|
6048
|
-
return io.which('npx', true).then((npxPath) => {
|
|
6049
|
-
return exec.exec(
|
|
6050
|
-
quote(npxPath),
|
|
6051
|
-
['cypress', 'cache', 'list'],
|
|
6052
|
-
cypressCommandOptions
|
|
6053
|
-
)
|
|
6054
|
-
})
|
|
6055
|
-
}
|
|
6056
|
-
|
|
6057
|
-
const verifyCypressBinary = () => {
|
|
6058
|
-
debug(
|
|
6059
|
-
`Verifying Cypress using cache folder ${CYPRESS_CACHE_FOLDER}`
|
|
6060
|
-
)
|
|
6061
|
-
if (isCypressBinarySkipped()) {
|
|
6062
|
-
debug('Skipping Cypress verify, binary is not installed')
|
|
6063
|
-
return Promise.resolve()
|
|
6064
|
-
}
|
|
6065
|
-
|
|
6066
|
-
core.exportVariable('CYPRESS_CACHE_FOLDER', CYPRESS_CACHE_FOLDER)
|
|
6067
|
-
return io.which('npx', true).then((npxPath) => {
|
|
6068
|
-
return exec.exec(
|
|
6069
|
-
quote(npxPath),
|
|
6070
|
-
['cypress', 'verify'],
|
|
6071
|
-
cypressCommandOptions
|
|
6072
|
-
)
|
|
6073
|
-
})
|
|
6074
|
-
}
|
|
6075
|
-
|
|
6076
|
-
/**
|
|
6077
|
-
* Grabs a boolean GitHub Action parameter input and casts it.
|
|
6078
|
-
* @param {string} name - parameter name
|
|
6079
|
-
* @param {boolean} defaultValue - default value to use if the parameter was not specified
|
|
6080
|
-
* @returns {boolean} converted input argument or default value
|
|
6081
|
-
*/
|
|
6082
|
-
const getInputBool = (name, defaultValue = false) => {
|
|
6083
|
-
const param = core.getInput(name)
|
|
6084
|
-
if (param === 'true' || param === '1') {
|
|
6085
|
-
return true
|
|
6086
|
-
}
|
|
6087
|
-
if (param === 'false' || param === '0') {
|
|
6088
|
-
return false
|
|
6089
|
-
}
|
|
6090
|
-
|
|
6091
|
-
return defaultValue
|
|
6092
|
-
}
|
|
6093
|
-
|
|
6094
|
-
/**
|
|
6095
|
-
* Grabs the spec input from the workflow and normalizes
|
|
6096
|
-
* it, since sometimes it can be multiline
|
|
6097
|
-
* @returns {string|undefined}
|
|
6098
|
-
*/
|
|
6099
|
-
const getSpecsList = () => {
|
|
6100
|
-
const spec = core.getInput('spec')
|
|
6101
|
-
if (!spec) {
|
|
6102
|
-
return
|
|
6103
|
-
}
|
|
6104
|
-
const specLines = spec.split('\n').join(',')
|
|
6105
|
-
debug(`extracted spec lines into: "${specLines}"`)
|
|
6106
|
-
return specLines
|
|
6107
|
-
}
|
|
6108
|
-
|
|
6109
|
-
const buildAppMaybe = () => {
|
|
6110
|
-
const buildApp = core.getInput('build')
|
|
6111
|
-
if (!buildApp) {
|
|
6112
|
-
return
|
|
6113
|
-
}
|
|
6114
|
-
|
|
6115
|
-
debug(`building application using "${buildApp}"`)
|
|
6116
|
-
|
|
6117
|
-
return execCommand(buildApp, true, 'build app')
|
|
6118
|
-
}
|
|
6119
|
-
|
|
6120
|
-
const startServersMaybe = () => {
|
|
6121
|
-
let startCommand
|
|
6122
|
-
|
|
6123
|
-
if (isWindows()) {
|
|
6124
|
-
// allow custom Windows start command
|
|
6125
|
-
startCommand =
|
|
6126
|
-
core.getInput('start-windows') || core.getInput('start')
|
|
6127
|
-
} else {
|
|
6128
|
-
startCommand = core.getInput('start')
|
|
6129
|
-
}
|
|
6130
|
-
if (!startCommand) {
|
|
6131
|
-
debug('No start command found')
|
|
6132
|
-
return Promise.resolve()
|
|
6133
|
-
}
|
|
6134
|
-
|
|
6135
|
-
// allow commands to be separated using commas or newlines
|
|
6136
|
-
const separateStartCommands = startCommand
|
|
6137
|
-
.split(/,|\n/)
|
|
6138
|
-
.map((s) => s.trim())
|
|
6139
|
-
.filter(Boolean)
|
|
6140
|
-
debug(
|
|
6141
|
-
`Separated ${
|
|
6142
|
-
separateStartCommands.length
|
|
6143
|
-
} start commands ${separateStartCommands.join(', ')}`
|
|
6144
|
-
)
|
|
6145
|
-
|
|
6146
|
-
return separateStartCommands.map((startCommand) => {
|
|
6147
|
-
return execCommand(
|
|
6148
|
-
startCommand,
|
|
6149
|
-
false,
|
|
6150
|
-
`start server "${startCommand}`
|
|
6151
|
-
)
|
|
6152
|
-
})
|
|
6153
|
-
}
|
|
6154
|
-
|
|
6155
|
-
/**
|
|
6156
|
-
* Pings give URL(s) until the timeout expires.
|
|
6157
|
-
* @param {string} waitOn A single URL or comma-separated URLs
|
|
6158
|
-
* @param {Number?} waitOnTimeout in seconds
|
|
6159
|
-
*/
|
|
6160
|
-
const waitOnUrl = (waitOn, waitOnTimeout = 60) => {
|
|
6161
|
-
console.log(
|
|
6162
|
-
'waiting on "%s" with timeout of %s seconds',
|
|
6163
|
-
waitOn,
|
|
6164
|
-
waitOnTimeout
|
|
6165
|
-
)
|
|
6166
|
-
|
|
6167
|
-
const waitTimeoutMs = waitOnTimeout * 1000
|
|
6168
|
-
|
|
6169
|
-
const waitUrls = waitOn
|
|
6170
|
-
.split(',')
|
|
6171
|
-
.map((s) => s.trim())
|
|
6172
|
-
.filter(Boolean)
|
|
6173
|
-
debug(`Waiting for urls ${waitUrls.join(', ')}`)
|
|
6174
|
-
|
|
6175
|
-
// run every wait promise after the previous has finished
|
|
6176
|
-
// to avoid "noise" of debug messages
|
|
6177
|
-
return waitUrls.reduce((prevPromise, url) => {
|
|
6178
|
-
return prevPromise.then(() => {
|
|
6179
|
-
debug(`Waiting for url ${url}`)
|
|
6180
|
-
return ping(url, waitTimeoutMs)
|
|
6181
|
-
})
|
|
6182
|
-
}, Promise.resolve())
|
|
6183
|
-
}
|
|
6184
|
-
|
|
6185
|
-
const waitOnMaybe = () => {
|
|
6186
|
-
const waitOn = core.getInput('wait-on')
|
|
6187
|
-
if (!waitOn) {
|
|
6188
|
-
return
|
|
6189
|
-
}
|
|
6190
|
-
|
|
6191
|
-
const waitOnTimeout = core.getInput('wait-on-timeout') || '60'
|
|
6192
|
-
const timeoutSeconds = parseFloat(waitOnTimeout)
|
|
6193
|
-
|
|
6194
|
-
if (isUrl(waitOn)) {
|
|
6195
|
-
return waitOnUrl(waitOn, timeoutSeconds)
|
|
6196
|
-
}
|
|
6197
|
-
|
|
6198
|
-
console.log('Waiting using command "%s"', waitOn)
|
|
6199
|
-
return execCommand(waitOn, true)
|
|
6200
|
-
}
|
|
6201
|
-
|
|
6202
|
-
const I = (x) => x
|
|
6203
|
-
|
|
6204
|
-
/**
|
|
6205
|
-
* Asks Cypress API if there were already builds for this commit.
|
|
6206
|
-
* In that case increments the count to get unique parallel id.
|
|
6207
|
-
*/
|
|
6208
|
-
const getCiBuildId = async () => {
|
|
6209
|
-
const {
|
|
6210
|
-
GITHUB_WORKFLOW,
|
|
6211
|
-
GITHUB_SHA,
|
|
6212
|
-
GITHUB_TOKEN,
|
|
6213
|
-
GITHUB_RUN_ID,
|
|
6214
|
-
GITHUB_REPOSITORY
|
|
6215
|
-
} = process.env
|
|
6216
|
-
|
|
6217
|
-
const [owner, repo] = GITHUB_REPOSITORY.split('/')
|
|
6218
|
-
let branch
|
|
6219
|
-
let buildId = `${GITHUB_WORKFLOW} - ${GITHUB_SHA}`
|
|
6220
|
-
|
|
6221
|
-
if (GITHUB_TOKEN) {
|
|
6222
|
-
debug(
|
|
6223
|
-
`Determining build id by asking GitHub about run ${GITHUB_RUN_ID}`
|
|
6224
|
-
)
|
|
6225
|
-
|
|
6226
|
-
const client = new Octokit({
|
|
6227
|
-
auth: GITHUB_TOKEN
|
|
6228
|
-
})
|
|
6229
|
-
|
|
6230
|
-
const resp = await client.request(
|
|
6231
|
-
'GET /repos/:owner/:repo/actions/runs/:run_id',
|
|
6232
|
-
{
|
|
6233
|
-
owner,
|
|
6234
|
-
repo,
|
|
6235
|
-
run_id: parseInt(GITHUB_RUN_ID)
|
|
6236
|
-
}
|
|
6237
|
-
)
|
|
6238
|
-
|
|
6239
|
-
if (resp && resp.data && resp.data.head_branch) {
|
|
6240
|
-
branch = resp.data.head_branch
|
|
6241
|
-
debug(`found the branch name ${branch}`)
|
|
6242
|
-
}
|
|
6243
|
-
|
|
6244
|
-
// This will return the complete list of jobs for a run with their steps,
|
|
6245
|
-
// this should always return data when there are jobs on the workflow.
|
|
6246
|
-
// Every time the workflow is re-run the jobs length should stay the same
|
|
6247
|
-
// (because the same amount of jobs were ran) but the id of them should change
|
|
6248
|
-
// letting us, select the first id as unique id
|
|
6249
|
-
// https://docs.github.com/en/rest/reference/actions#list-jobs-for-a-workflow-run
|
|
6250
|
-
const runsList = await client.request(
|
|
6251
|
-
'GET /repos/:owner/:repo/actions/runs/:run_id/jobs',
|
|
6252
|
-
{
|
|
6253
|
-
owner,
|
|
6254
|
-
repo,
|
|
6255
|
-
run_id: parseInt(GITHUB_RUN_ID)
|
|
6256
|
-
}
|
|
6257
|
-
)
|
|
6258
|
-
|
|
6259
|
-
if (
|
|
6260
|
-
runsList &&
|
|
6261
|
-
runsList.data &&
|
|
6262
|
-
runsList.data.jobs &&
|
|
6263
|
-
runsList.data.jobs.length
|
|
6264
|
-
) {
|
|
6265
|
-
const jobId = runsList.data.jobs[0].id
|
|
6266
|
-
debug(`fetched run list with jobId ${jobId}`)
|
|
6267
|
-
buildId = `${GITHUB_RUN_ID}-${jobId}`
|
|
6268
|
-
} else {
|
|
6269
|
-
debug('could not get run list data')
|
|
6270
|
-
}
|
|
6271
|
-
}
|
|
6272
|
-
|
|
6273
|
-
debug(`determined branch ${branch} and build id ${buildId}`)
|
|
6274
|
-
return { branch, buildId }
|
|
6275
|
-
}
|
|
6276
|
-
|
|
6277
|
-
/**
|
|
6278
|
-
* Forms entire command line like "npx cypress run ..."
|
|
6279
|
-
*/
|
|
6280
|
-
const runTestsUsingCommandLine = async () => {
|
|
6281
|
-
debug('Running Cypress tests using CLI command')
|
|
6282
|
-
const quoteArgument = isWindows() ? quoteWindowsArgument : I
|
|
6283
|
-
|
|
6284
|
-
const commandPrefix = core.getInput('command-prefix')
|
|
6285
|
-
if (!commandPrefix) {
|
|
6286
|
-
throw new Error('Expected command prefix')
|
|
6287
|
-
}
|
|
6288
|
-
|
|
6289
|
-
const record = getInputBool('record')
|
|
6290
|
-
const parallel = getInputBool('parallel')
|
|
6291
|
-
const headless = getInputBool('headless')
|
|
6292
|
-
|
|
6293
|
-
// TODO using yarn to run cypress when yarn is used for install
|
|
6294
|
-
// split potentially long command?
|
|
6295
|
-
|
|
6296
|
-
let cmd = []
|
|
6297
|
-
// we need to split the command prefix into individual arguments
|
|
6298
|
-
// otherwise they are passed all as a single string
|
|
6299
|
-
const parts = commandPrefix.split(' ')
|
|
6300
|
-
cmd = cmd.concat(parts)
|
|
6301
|
-
debug(`with concatenated command prefix: ${cmd.join(' ')}`)
|
|
6302
|
-
|
|
6303
|
-
// push each CLI argument separately
|
|
6304
|
-
cmd.push('cypress')
|
|
6305
|
-
cmd.push('run')
|
|
6306
|
-
if (headless) {
|
|
6307
|
-
cmd.push('--headless')
|
|
6308
|
-
}
|
|
6309
|
-
if (record) {
|
|
6310
|
-
cmd.push('--record')
|
|
6311
|
-
}
|
|
6312
|
-
if (parallel) {
|
|
6313
|
-
cmd.push('--parallel')
|
|
6314
|
-
}
|
|
6315
|
-
const group = core.getInput('group')
|
|
6316
|
-
if (group) {
|
|
6317
|
-
cmd.push('--group')
|
|
6318
|
-
cmd.push(quoteArgument(group))
|
|
6319
|
-
}
|
|
6320
|
-
const tag = core.getInput('tag')
|
|
6321
|
-
if (tag) {
|
|
6322
|
-
cmd.push('--tag')
|
|
6323
|
-
cmd.push(quoteArgument(tag))
|
|
6324
|
-
}
|
|
6325
|
-
const configInput = core.getInput('config')
|
|
6326
|
-
if (configInput) {
|
|
6327
|
-
cmd.push('--config')
|
|
6328
|
-
cmd.push(quoteArgument(configInput))
|
|
6329
|
-
}
|
|
6330
|
-
const spec = getSpecsList()
|
|
6331
|
-
if (spec) {
|
|
6332
|
-
cmd.push('--spec')
|
|
6333
|
-
cmd.push(quoteArgument(spec))
|
|
6334
|
-
}
|
|
6335
|
-
const project = core.getInput('project')
|
|
6336
|
-
if (project) {
|
|
6337
|
-
cmd.push('--project')
|
|
6338
|
-
cmd.push(quoteArgument(project))
|
|
6339
|
-
}
|
|
6340
|
-
const configFileInput = core.getInput('config-file')
|
|
6341
|
-
if (configFileInput) {
|
|
6342
|
-
cmd.push('--config-file')
|
|
6343
|
-
cmd.push(quoteArgument(configFileInput))
|
|
6344
|
-
}
|
|
6345
|
-
|
|
6346
|
-
if (parallel || group) {
|
|
6347
|
-
const { branch, buildId } = await getCiBuildId()
|
|
6348
|
-
if (branch) {
|
|
6349
|
-
core.exportVariable('GH_BRANCH', branch)
|
|
6350
|
-
}
|
|
6351
|
-
|
|
6352
|
-
cmd.push('--ci-build-id')
|
|
6353
|
-
const ciBuildId = core.getInput('ci-build-id') || buildId
|
|
6354
|
-
cmd.push(quoteArgument(ciBuildId))
|
|
6355
|
-
}
|
|
6356
|
-
|
|
6357
|
-
const browser = core.getInput('browser')
|
|
6358
|
-
if (browser) {
|
|
6359
|
-
cmd.push('--browser')
|
|
6360
|
-
// TODO should browser be quoted?
|
|
6361
|
-
// If it is a path, it might have spaces
|
|
6362
|
-
cmd.push(browser)
|
|
6363
|
-
}
|
|
6364
|
-
|
|
6365
|
-
const envInput = core.getInput('env')
|
|
6366
|
-
if (envInput) {
|
|
6367
|
-
// TODO should env be quoted?
|
|
6368
|
-
// If it is a JSON, it might have spaces
|
|
6369
|
-
cmd.push('--env')
|
|
6370
|
-
cmd.push(envInput)
|
|
6371
|
-
}
|
|
6372
|
-
|
|
6373
|
-
const quiet = getInputBool('quiet')
|
|
6374
|
-
if (quiet) {
|
|
6375
|
-
cmd.push('--quiet')
|
|
6376
|
-
}
|
|
6377
|
-
|
|
6378
|
-
console.log('Cypress test command: npx %s', cmd.join(' '))
|
|
6379
|
-
|
|
6380
|
-
// since we have quoted arguments ourselves, do not double quote them
|
|
6381
|
-
const opts = {
|
|
6382
|
-
...cypressCommandOptions,
|
|
6383
|
-
windowsVerbatimArguments: false
|
|
6384
|
-
}
|
|
6385
|
-
|
|
6386
|
-
debug(`in working directory "${cypressCommandOptions.cwd}"`)
|
|
6387
|
-
|
|
6388
|
-
const npxPath = await io.which('npx', true)
|
|
6389
|
-
debug(`npx path: ${npxPath}`)
|
|
6390
|
-
|
|
6391
|
-
return exec.exec(quote(npxPath), cmd, opts)
|
|
6392
|
-
}
|
|
6393
|
-
|
|
6394
|
-
/**
|
|
6395
|
-
* Run Cypress tests by collecting input parameters
|
|
6396
|
-
* and using Cypress module API to run tests.
|
|
6397
|
-
* @see https://on.cypress.io/module-api
|
|
6398
|
-
*/
|
|
6399
|
-
const runTests = async () => {
|
|
6400
|
-
const runTests = getInputBool('runTests', true)
|
|
6401
|
-
if (!runTests) {
|
|
6402
|
-
console.log('Skipping running tests: runTests parameter is false')
|
|
6403
|
-
return
|
|
6404
|
-
}
|
|
6405
|
-
|
|
6406
|
-
// export common environment variables that help run Cypress
|
|
6407
|
-
core.exportVariable('CYPRESS_CACHE_FOLDER', CYPRESS_CACHE_FOLDER)
|
|
6408
|
-
core.exportVariable('TERM', 'xterm')
|
|
6409
|
-
|
|
6410
|
-
const customCommand = core.getInput('command')
|
|
6411
|
-
if (customCommand) {
|
|
6412
|
-
console.log('Using custom test command: %s', customCommand)
|
|
6413
|
-
return execCommand(customCommand, true, 'run tests')
|
|
6414
|
-
}
|
|
6415
|
-
|
|
6416
|
-
const commandPrefix = core.getInput('command-prefix')
|
|
6417
|
-
if (commandPrefix) {
|
|
6418
|
-
return runTestsUsingCommandLine()
|
|
6419
|
-
}
|
|
6420
|
-
|
|
6421
|
-
debug('Running Cypress tests using NPM module API')
|
|
6422
|
-
debug(`requiring cypress dependency, cwd is ${process.cwd()}`)
|
|
6423
|
-
debug(`working directory ${workingDirectory}`)
|
|
6424
|
-
const cypressModulePath =
|
|
6425
|
-
require.resolve('cypress', {
|
|
6426
|
-
paths: [workingDirectory]
|
|
6427
|
-
}) || 'cypress'
|
|
6428
|
-
debug(`resolved cypress ${cypressModulePath}`)
|
|
6429
|
-
|
|
6430
|
-
const cypress = require(cypressModulePath)
|
|
6431
|
-
const cypressOptions = {
|
|
6432
|
-
headless: getInputBool('headless'),
|
|
6433
|
-
record: getInputBool('record'),
|
|
6434
|
-
parallel: getInputBool('parallel'),
|
|
6435
|
-
quiet: getInputBool('quiet')
|
|
6436
|
-
}
|
|
6437
|
-
|
|
6438
|
-
if (core.getInput('group')) {
|
|
6439
|
-
cypressOptions.group = core.getInput('group')
|
|
6440
|
-
}
|
|
6441
|
-
if (core.getInput('tag')) {
|
|
6442
|
-
cypressOptions.tag = core.getInput('tag')
|
|
6443
|
-
}
|
|
6444
|
-
if (core.getInput('config')) {
|
|
6445
|
-
cypressOptions.config = core.getInput('config')
|
|
6446
|
-
debug(`Cypress config "${cypressOptions.config}"`)
|
|
6447
|
-
}
|
|
6448
|
-
const spec = getSpecsList()
|
|
6449
|
-
if (spec) {
|
|
6450
|
-
cypressOptions.spec = spec
|
|
6451
|
-
}
|
|
6452
|
-
if (core.getInput('config-file')) {
|
|
6453
|
-
cypressOptions.configFile = core.getInput('config-file')
|
|
6454
|
-
}
|
|
6455
|
-
|
|
6456
|
-
// if the user set the explicit folder, use that
|
|
6457
|
-
if (core.getInput('project')) {
|
|
6458
|
-
cypressOptions.project = core.getInput('project')
|
|
6459
|
-
}
|
|
6460
|
-
if (core.getInput('browser')) {
|
|
6461
|
-
cypressOptions.browser = core.getInput('browser')
|
|
6462
|
-
}
|
|
6463
|
-
if (core.getInput('env')) {
|
|
6464
|
-
cypressOptions.env = core.getInput('env')
|
|
6465
|
-
}
|
|
6466
|
-
|
|
6467
|
-
if (cypressOptions.parallel || cypressOptions.group) {
|
|
6468
|
-
const { branch, buildId } = await getCiBuildId()
|
|
6469
|
-
if (branch) {
|
|
6470
|
-
core.exportVariable('GH_BRANCH', branch)
|
|
6471
|
-
}
|
|
6472
|
-
|
|
6473
|
-
cypressOptions.ciBuildId = core.getInput('ci-build-id') || buildId
|
|
6474
|
-
}
|
|
6475
|
-
|
|
6476
|
-
debug(`Cypress options ${JSON.stringify(cypressOptions)}`)
|
|
6477
|
-
|
|
6478
|
-
const onTestsFinished = (testResults) => {
|
|
6479
|
-
process.chdir(startWorkingDirectory)
|
|
6480
|
-
|
|
6481
|
-
if (testResults.failures) {
|
|
6482
|
-
console.error('Test run failed, code %d', testResults.failures)
|
|
6483
|
-
console.error('More information might be available above')
|
|
6484
|
-
|
|
6485
|
-
if (testResults.message) {
|
|
6486
|
-
console.error(
|
|
6487
|
-
'Cypress module has returned the following error message:'
|
|
6488
|
-
)
|
|
6489
|
-
console.error(testResults.message)
|
|
6490
|
-
}
|
|
6491
|
-
|
|
6492
|
-
return Promise.reject(
|
|
6493
|
-
new Error(testResults.message || 'Error running Cypress')
|
|
6494
|
-
)
|
|
6495
|
-
}
|
|
6496
|
-
|
|
6497
|
-
debug(`Cypress tests: ${testResults.totalFailed} failed`)
|
|
6498
|
-
|
|
6499
|
-
const dashboardUrl = testResults.runUrl
|
|
6500
|
-
if (dashboardUrl) {
|
|
6501
|
-
debug(`Dashboard url ${dashboardUrl}`)
|
|
6502
|
-
} else {
|
|
6503
|
-
debug('There is no Dashboard url')
|
|
6504
|
-
}
|
|
6505
|
-
// we still set the output explicitly
|
|
6506
|
-
core.setOutput('dashboardUrl', dashboardUrl)
|
|
6507
|
-
|
|
6508
|
-
if (testResults.totalFailed) {
|
|
6509
|
-
return Promise.reject(
|
|
6510
|
-
new Error(`Cypress tests: ${testResults.totalFailed} failed`)
|
|
6511
|
-
)
|
|
6512
|
-
}
|
|
6513
|
-
}
|
|
6514
|
-
|
|
6515
|
-
const onTestsError = (e) => {
|
|
6516
|
-
process.chdir(startWorkingDirectory)
|
|
6517
|
-
|
|
6518
|
-
console.error(e)
|
|
6519
|
-
return Promise.reject(e)
|
|
6520
|
-
}
|
|
6521
|
-
|
|
6522
|
-
process.chdir(workingDirectory)
|
|
6523
|
-
return cypress
|
|
6524
|
-
.run(cypressOptions)
|
|
6525
|
-
.then(onTestsFinished, onTestsError)
|
|
6526
|
-
}
|
|
6527
|
-
|
|
6528
|
-
const installMaybe = () => {
|
|
6529
|
-
const installParameter = getInputBool('install', true)
|
|
6530
|
-
if (!installParameter) {
|
|
6531
|
-
console.log('Skipping install because install parameter is false')
|
|
6532
|
-
return Promise.resolve()
|
|
6533
|
-
}
|
|
6534
|
-
|
|
6535
|
-
return Promise.all([
|
|
6536
|
-
restoreCachedNpm(),
|
|
6537
|
-
restoreCachedCypressBinary()
|
|
6538
|
-
]).then(([npmCacheHit, cypressCacheHit]) => {
|
|
6539
|
-
debug(`npm cache hit ${npmCacheHit}`)
|
|
6540
|
-
debug(`cypress cache hit ${cypressCacheHit}`)
|
|
6541
|
-
|
|
6542
|
-
return install().then(() => {
|
|
6543
|
-
debug('install has finished')
|
|
6544
|
-
return listCypressBinaries().then(() => {
|
|
6545
|
-
if (npmCacheHit && cypressCacheHit) {
|
|
6546
|
-
debug('no need to verify Cypress binary or save caches')
|
|
6547
|
-
return Promise.resolve(undefined)
|
|
6548
|
-
}
|
|
6549
|
-
|
|
6550
|
-
debug('verifying Cypress binary')
|
|
6551
|
-
return verifyCypressBinary()
|
|
6552
|
-
.then(saveCachedNpm)
|
|
6553
|
-
.then(saveCachedCypressBinary)
|
|
6554
|
-
})
|
|
6555
|
-
})
|
|
6556
|
-
})
|
|
6557
|
-
}
|
|
6558
|
-
|
|
6559
|
-
installMaybe()
|
|
6560
|
-
.then(buildAppMaybe)
|
|
6561
|
-
.then(startServersMaybe)
|
|
6562
|
-
.then(waitOnMaybe)
|
|
6563
|
-
.then(runTests)
|
|
6564
|
-
.then(() => {
|
|
6565
|
-
debug('all done, exiting')
|
|
6566
|
-
// force exit to avoid waiting for child processes,
|
|
6567
|
-
// like the server we have started
|
|
6568
|
-
// see https://github.com/actions/toolkit/issues/216
|
|
6569
|
-
process.exit(0)
|
|
6570
|
-
})
|
|
6571
|
-
.catch((error) => {
|
|
6572
|
-
// final catch - when anything goes wrong, throw an error
|
|
6573
|
-
// and exit the action with non-zero code
|
|
6574
|
-
debug(error.message)
|
|
6575
|
-
debug(error.stack)
|
|
6576
|
-
|
|
6577
|
-
core.setFailed(error.message)
|
|
6578
|
-
process.exit(1)
|
|
6579
|
-
})
|
|
5794
|
+
// @ts-check
|
|
5795
|
+
const { restoreCache, saveCache } = __webpack_require__(692)
|
|
5796
|
+
const core = __webpack_require__(470)
|
|
5797
|
+
const exec = __webpack_require__(986)
|
|
5798
|
+
const io = __webpack_require__(1)
|
|
5799
|
+
const { Octokit } = __webpack_require__(448)
|
|
5800
|
+
const hasha = __webpack_require__(309)
|
|
5801
|
+
const fs = __webpack_require__(747)
|
|
5802
|
+
const os = __webpack_require__(87)
|
|
5803
|
+
const path = __webpack_require__(622)
|
|
5804
|
+
const quote = __webpack_require__(531)
|
|
5805
|
+
const cliParser = __webpack_require__(324)()
|
|
5806
|
+
const findYarnWorkspaceRoot = __webpack_require__(638)
|
|
5807
|
+
const debug = __webpack_require__(784)('@cypress/github-action')
|
|
5808
|
+
const { ping } = __webpack_require__(946)
|
|
5809
|
+
|
|
5810
|
+
/**
|
|
5811
|
+
* Parses input command, finds the tool and
|
|
5812
|
+
* the runs the command.
|
|
5813
|
+
*/
|
|
5814
|
+
const execCommand = (
|
|
5815
|
+
fullCommand,
|
|
5816
|
+
waitToFinish = true,
|
|
5817
|
+
label = 'executing'
|
|
5818
|
+
) => {
|
|
5819
|
+
const cwd = cypressCommandOptions.cwd
|
|
5820
|
+
|
|
5821
|
+
console.log('%s command "%s"', label, fullCommand)
|
|
5822
|
+
console.log('current working directory "%s"', cwd)
|
|
5823
|
+
|
|
5824
|
+
const args = cliParser.parse(fullCommand)
|
|
5825
|
+
debug(`parsed command: ${args.join(' ')}`)
|
|
5826
|
+
|
|
5827
|
+
return io.which(args[0], true).then((toolPath) => {
|
|
5828
|
+
debug(`found command "${toolPath}"`)
|
|
5829
|
+
debug(`with arguments ${args.slice(1).join(' ')}`)
|
|
5830
|
+
|
|
5831
|
+
const toolArguments = args.slice(1)
|
|
5832
|
+
const argsString = toolArguments.join(' ')
|
|
5833
|
+
debug(`running ${quote(toolPath)} ${argsString} in ${cwd}`)
|
|
5834
|
+
debug(`waiting for the command to finish? ${waitToFinish}`)
|
|
5835
|
+
|
|
5836
|
+
const promise = exec.exec(
|
|
5837
|
+
quote(toolPath),
|
|
5838
|
+
toolArguments,
|
|
5839
|
+
cypressCommandOptions
|
|
5840
|
+
)
|
|
5841
|
+
if (waitToFinish) {
|
|
5842
|
+
return promise
|
|
5843
|
+
}
|
|
5844
|
+
})
|
|
5845
|
+
}
|
|
5846
|
+
|
|
5847
|
+
const isWindows = () => os.platform() === 'win32'
|
|
5848
|
+
const isUrl = (s) => /^https?:\/\//.test(s)
|
|
5849
|
+
|
|
5850
|
+
/**
|
|
5851
|
+
* Quote strings that contain Windows delimiters.
|
|
5852
|
+
* https://github.com/cypress-io/github-action/issues/459
|
|
5853
|
+
*/
|
|
5854
|
+
const quoteWindowsArgument = (value) => {
|
|
5855
|
+
if (!/[,;=\s]/.test(value)) {
|
|
5856
|
+
return value
|
|
5857
|
+
}
|
|
5858
|
+
return quote(value)
|
|
5859
|
+
}
|
|
5860
|
+
|
|
5861
|
+
/**
|
|
5862
|
+
* Returns true if the Cypress binary installation was skipped
|
|
5863
|
+
* via an environment variable https://on.cypress.io/installing
|
|
5864
|
+
*/
|
|
5865
|
+
const isCypressBinarySkipped = () =>
|
|
5866
|
+
process.env.CYPRESS_INSTALL_BINARY === '0'
|
|
5867
|
+
|
|
5868
|
+
const homeDirectory = os.homedir()
|
|
5869
|
+
const platformAndArch = `${process.platform}-${process.arch}`
|
|
5870
|
+
|
|
5871
|
+
const startWorkingDirectory = process.cwd()
|
|
5872
|
+
// seems the working directory should be absolute to work correctly
|
|
5873
|
+
// https://github.com/cypress-io/github-action/issues/211
|
|
5874
|
+
const workingDirectory = core.getInput('working-directory')
|
|
5875
|
+
? path.resolve(core.getInput('working-directory'))
|
|
5876
|
+
: startWorkingDirectory
|
|
5877
|
+
debug(`working directory ${workingDirectory}`)
|
|
5878
|
+
|
|
5879
|
+
/**
|
|
5880
|
+
* When running "npm install" or any other Cypress-related commands,
|
|
5881
|
+
* use the install directory as current working directory
|
|
5882
|
+
*/
|
|
5883
|
+
const cypressCommandOptions = {
|
|
5884
|
+
cwd: workingDirectory
|
|
5885
|
+
}
|
|
5886
|
+
|
|
5887
|
+
const yarnFilename = path.join(
|
|
5888
|
+
findYarnWorkspaceRoot(workingDirectory) || workingDirectory,
|
|
5889
|
+
'yarn.lock'
|
|
5890
|
+
)
|
|
5891
|
+
const packageLockFilename = path.join(
|
|
5892
|
+
workingDirectory,
|
|
5893
|
+
'package-lock.json'
|
|
5894
|
+
)
|
|
5895
|
+
|
|
5896
|
+
const useYarn = () => fs.existsSync(yarnFilename)
|
|
5897
|
+
|
|
5898
|
+
const lockHash = () => {
|
|
5899
|
+
const lockFilename = useYarn() ? yarnFilename : packageLockFilename
|
|
5900
|
+
const fileHash = hasha.fromFileSync(lockFilename)
|
|
5901
|
+
debug(`Hash from file ${lockFilename} is ${fileHash}`)
|
|
5902
|
+
return fileHash
|
|
5903
|
+
}
|
|
5904
|
+
|
|
5905
|
+
// enforce the same NPM cache folder across different operating systems
|
|
5906
|
+
const NPM_CACHE_FOLDER = path.join(homeDirectory, '.npm')
|
|
5907
|
+
const getNpmCache = () => {
|
|
5908
|
+
const o = {}
|
|
5909
|
+
let key = core.getInput('cache-key')
|
|
5910
|
+
const hash = lockHash()
|
|
5911
|
+
if (!key) {
|
|
5912
|
+
if (useYarn()) {
|
|
5913
|
+
key = `yarn-${platformAndArch}-${hash}`
|
|
5914
|
+
} else {
|
|
5915
|
+
key = `npm-${platformAndArch}-${hash}`
|
|
5916
|
+
}
|
|
5917
|
+
} else {
|
|
5918
|
+
console.log('using custom cache key "%s"', key)
|
|
5919
|
+
}
|
|
5920
|
+
|
|
5921
|
+
if (useYarn()) {
|
|
5922
|
+
o.inputPath = path.join(homeDirectory, '.cache', 'yarn')
|
|
5923
|
+
} else {
|
|
5924
|
+
o.inputPath = NPM_CACHE_FOLDER
|
|
5925
|
+
}
|
|
5926
|
+
|
|
5927
|
+
// use exact restore key to prevent NPM cache from growing
|
|
5928
|
+
// https://glebbahmutov.com/blog/do-not-let-npm-cache-snowball/
|
|
5929
|
+
o.restoreKeys = o.primaryKey = key
|
|
5930
|
+
return o
|
|
5931
|
+
}
|
|
5932
|
+
|
|
5933
|
+
// custom Cypress binary cache folder
|
|
5934
|
+
// see https://on.cypress.io/caching
|
|
5935
|
+
const CYPRESS_CACHE_FOLDER =
|
|
5936
|
+
process.env.CYPRESS_CACHE_FOLDER ||
|
|
5937
|
+
path.join(homeDirectory, '.cache', 'Cypress')
|
|
5938
|
+
debug(`using custom Cypress cache folder "${CYPRESS_CACHE_FOLDER}"`)
|
|
5939
|
+
|
|
5940
|
+
const getCypressBinaryCache = () => {
|
|
5941
|
+
const o = {
|
|
5942
|
+
inputPath: CYPRESS_CACHE_FOLDER
|
|
5943
|
+
}
|
|
5944
|
+
const hash = lockHash()
|
|
5945
|
+
const key = `cypress-${platformAndArch}-${hash}`
|
|
5946
|
+
|
|
5947
|
+
// use only exact restore key to prevent cached folder growing in size
|
|
5948
|
+
// https://glebbahmutov.com/blog/do-not-let-cypress-cache-snowball/
|
|
5949
|
+
o.restoreKeys = o.primaryKey = key
|
|
5950
|
+
return o
|
|
5951
|
+
}
|
|
5952
|
+
|
|
5953
|
+
const restoreCachedNpm = () => {
|
|
5954
|
+
debug('trying to restore cached NPM modules')
|
|
5955
|
+
const NPM_CACHE = getNpmCache()
|
|
5956
|
+
return restoreCache([NPM_CACHE.inputPath], NPM_CACHE.primaryKey, [
|
|
5957
|
+
NPM_CACHE.restoreKeys
|
|
5958
|
+
]).catch((e) => {
|
|
5959
|
+
console.warn('Restoring NPM cache error: %s', e.message)
|
|
5960
|
+
})
|
|
5961
|
+
}
|
|
5962
|
+
|
|
5963
|
+
const saveCachedNpm = () => {
|
|
5964
|
+
debug('saving NPM modules')
|
|
5965
|
+
const NPM_CACHE = getNpmCache()
|
|
5966
|
+
return saveCache([NPM_CACHE.inputPath], NPM_CACHE.primaryKey).catch(
|
|
5967
|
+
(e) => {
|
|
5968
|
+
console.warn('Saving NPM cache error: %s', e.message)
|
|
5969
|
+
}
|
|
5970
|
+
)
|
|
5971
|
+
}
|
|
5972
|
+
|
|
5973
|
+
const restoreCachedCypressBinary = () => {
|
|
5974
|
+
debug('trying to restore cached Cypress binary')
|
|
5975
|
+
const CYPRESS_BINARY_CACHE = getCypressBinaryCache()
|
|
5976
|
+
return restoreCache(
|
|
5977
|
+
[CYPRESS_BINARY_CACHE.inputPath],
|
|
5978
|
+
CYPRESS_BINARY_CACHE.primaryKey,
|
|
5979
|
+
[CYPRESS_BINARY_CACHE.restoreKeys]
|
|
5980
|
+
).catch((e) => {
|
|
5981
|
+
console.warn('Restoring Cypress cache error: %s', e.message)
|
|
5982
|
+
})
|
|
5983
|
+
}
|
|
5984
|
+
|
|
5985
|
+
const saveCachedCypressBinary = () => {
|
|
5986
|
+
debug('saving Cypress binary')
|
|
5987
|
+
|
|
5988
|
+
if (isCypressBinarySkipped()) {
|
|
5989
|
+
debug('Skipping Cypress cache save, binary is not installed')
|
|
5990
|
+
return Promise.resolve()
|
|
5991
|
+
}
|
|
5992
|
+
|
|
5993
|
+
const CYPRESS_BINARY_CACHE = getCypressBinaryCache()
|
|
5994
|
+
return saveCache(
|
|
5995
|
+
[CYPRESS_BINARY_CACHE.inputPath],
|
|
5996
|
+
CYPRESS_BINARY_CACHE.primaryKey
|
|
5997
|
+
).catch((e) => {
|
|
5998
|
+
console.warn('Saving Cypress cache error: %s', e.message)
|
|
5999
|
+
})
|
|
6000
|
+
}
|
|
6001
|
+
|
|
6002
|
+
const install = () => {
|
|
6003
|
+
// prevent lots of progress messages during install
|
|
6004
|
+
core.exportVariable('CI', '1')
|
|
6005
|
+
core.exportVariable('CYPRESS_CACHE_FOLDER', CYPRESS_CACHE_FOLDER)
|
|
6006
|
+
// set NPM cache path in case the user has custom install command
|
|
6007
|
+
core.exportVariable('npm_config_cache', NPM_CACHE_FOLDER)
|
|
6008
|
+
|
|
6009
|
+
// Note: need to quote found tool to avoid Windows choking on
|
|
6010
|
+
// npm paths with spaces like "C:\Program Files\nodejs\npm.cmd ci"
|
|
6011
|
+
const installCommand = core.getInput('install-command')
|
|
6012
|
+
if (installCommand) {
|
|
6013
|
+
debug(`using custom install command "${installCommand}"`)
|
|
6014
|
+
return execCommand(installCommand, true, 'install command')
|
|
6015
|
+
}
|
|
6016
|
+
|
|
6017
|
+
if (useYarn()) {
|
|
6018
|
+
debug('installing NPM dependencies using Yarn')
|
|
6019
|
+
return io.which('yarn', true).then((yarnPath) => {
|
|
6020
|
+
debug(`yarn at "${yarnPath}"`)
|
|
6021
|
+
return exec.exec(
|
|
6022
|
+
quote(yarnPath),
|
|
6023
|
+
['--frozen-lockfile'],
|
|
6024
|
+
cypressCommandOptions
|
|
6025
|
+
)
|
|
6026
|
+
})
|
|
6027
|
+
} else {
|
|
6028
|
+
debug('installing NPM dependencies')
|
|
6029
|
+
|
|
6030
|
+
return io.which('npm', true).then((npmPath) => {
|
|
6031
|
+
debug(`npm at "${npmPath}"`)
|
|
6032
|
+
return exec.exec(quote(npmPath), ['ci'], cypressCommandOptions)
|
|
6033
|
+
})
|
|
6034
|
+
}
|
|
6035
|
+
}
|
|
6036
|
+
|
|
6037
|
+
const listCypressBinaries = () => {
|
|
6038
|
+
debug(
|
|
6039
|
+
`Cypress versions in the cache folder ${CYPRESS_CACHE_FOLDER}`
|
|
6040
|
+
)
|
|
6041
|
+
|
|
6042
|
+
if (isCypressBinarySkipped()) {
|
|
6043
|
+
debug('Skipping Cypress cache list, binary is not installed')
|
|
6044
|
+
return Promise.resolve()
|
|
6045
|
+
}
|
|
6046
|
+
|
|
6047
|
+
core.exportVariable('CYPRESS_CACHE_FOLDER', CYPRESS_CACHE_FOLDER)
|
|
6048
|
+
return io.which('npx', true).then((npxPath) => {
|
|
6049
|
+
return exec.exec(
|
|
6050
|
+
quote(npxPath),
|
|
6051
|
+
['cypress', 'cache', 'list'],
|
|
6052
|
+
cypressCommandOptions
|
|
6053
|
+
)
|
|
6054
|
+
})
|
|
6055
|
+
}
|
|
6056
|
+
|
|
6057
|
+
const verifyCypressBinary = () => {
|
|
6058
|
+
debug(
|
|
6059
|
+
`Verifying Cypress using cache folder ${CYPRESS_CACHE_FOLDER}`
|
|
6060
|
+
)
|
|
6061
|
+
if (isCypressBinarySkipped()) {
|
|
6062
|
+
debug('Skipping Cypress verify, binary is not installed')
|
|
6063
|
+
return Promise.resolve()
|
|
6064
|
+
}
|
|
6065
|
+
|
|
6066
|
+
core.exportVariable('CYPRESS_CACHE_FOLDER', CYPRESS_CACHE_FOLDER)
|
|
6067
|
+
return io.which('npx', true).then((npxPath) => {
|
|
6068
|
+
return exec.exec(
|
|
6069
|
+
quote(npxPath),
|
|
6070
|
+
['cypress', 'verify'],
|
|
6071
|
+
cypressCommandOptions
|
|
6072
|
+
)
|
|
6073
|
+
})
|
|
6074
|
+
}
|
|
6075
|
+
|
|
6076
|
+
/**
|
|
6077
|
+
* Grabs a boolean GitHub Action parameter input and casts it.
|
|
6078
|
+
* @param {string} name - parameter name
|
|
6079
|
+
* @param {boolean} defaultValue - default value to use if the parameter was not specified
|
|
6080
|
+
* @returns {boolean} converted input argument or default value
|
|
6081
|
+
*/
|
|
6082
|
+
const getInputBool = (name, defaultValue = false) => {
|
|
6083
|
+
const param = core.getInput(name)
|
|
6084
|
+
if (param === 'true' || param === '1') {
|
|
6085
|
+
return true
|
|
6086
|
+
}
|
|
6087
|
+
if (param === 'false' || param === '0') {
|
|
6088
|
+
return false
|
|
6089
|
+
}
|
|
6090
|
+
|
|
6091
|
+
return defaultValue
|
|
6092
|
+
}
|
|
6093
|
+
|
|
6094
|
+
/**
|
|
6095
|
+
* Grabs the spec input from the workflow and normalizes
|
|
6096
|
+
* it, since sometimes it can be multiline
|
|
6097
|
+
* @returns {string|undefined}
|
|
6098
|
+
*/
|
|
6099
|
+
const getSpecsList = () => {
|
|
6100
|
+
const spec = core.getInput('spec')
|
|
6101
|
+
if (!spec) {
|
|
6102
|
+
return
|
|
6103
|
+
}
|
|
6104
|
+
const specLines = spec.split('\n').join(',')
|
|
6105
|
+
debug(`extracted spec lines into: "${specLines}"`)
|
|
6106
|
+
return specLines
|
|
6107
|
+
}
|
|
6108
|
+
|
|
6109
|
+
const buildAppMaybe = () => {
|
|
6110
|
+
const buildApp = core.getInput('build')
|
|
6111
|
+
if (!buildApp) {
|
|
6112
|
+
return
|
|
6113
|
+
}
|
|
6114
|
+
|
|
6115
|
+
debug(`building application using "${buildApp}"`)
|
|
6116
|
+
|
|
6117
|
+
return execCommand(buildApp, true, 'build app')
|
|
6118
|
+
}
|
|
6119
|
+
|
|
6120
|
+
const startServersMaybe = () => {
|
|
6121
|
+
let startCommand
|
|
6122
|
+
|
|
6123
|
+
if (isWindows()) {
|
|
6124
|
+
// allow custom Windows start command
|
|
6125
|
+
startCommand =
|
|
6126
|
+
core.getInput('start-windows') || core.getInput('start')
|
|
6127
|
+
} else {
|
|
6128
|
+
startCommand = core.getInput('start')
|
|
6129
|
+
}
|
|
6130
|
+
if (!startCommand) {
|
|
6131
|
+
debug('No start command found')
|
|
6132
|
+
return Promise.resolve()
|
|
6133
|
+
}
|
|
6134
|
+
|
|
6135
|
+
// allow commands to be separated using commas or newlines
|
|
6136
|
+
const separateStartCommands = startCommand
|
|
6137
|
+
.split(/,|\n/)
|
|
6138
|
+
.map((s) => s.trim())
|
|
6139
|
+
.filter(Boolean)
|
|
6140
|
+
debug(
|
|
6141
|
+
`Separated ${
|
|
6142
|
+
separateStartCommands.length
|
|
6143
|
+
} start commands ${separateStartCommands.join(', ')}`
|
|
6144
|
+
)
|
|
6145
|
+
|
|
6146
|
+
return separateStartCommands.map((startCommand) => {
|
|
6147
|
+
return execCommand(
|
|
6148
|
+
startCommand,
|
|
6149
|
+
false,
|
|
6150
|
+
`start server "${startCommand}`
|
|
6151
|
+
)
|
|
6152
|
+
})
|
|
6153
|
+
}
|
|
6154
|
+
|
|
6155
|
+
/**
|
|
6156
|
+
* Pings give URL(s) until the timeout expires.
|
|
6157
|
+
* @param {string} waitOn A single URL or comma-separated URLs
|
|
6158
|
+
* @param {Number?} waitOnTimeout in seconds
|
|
6159
|
+
*/
|
|
6160
|
+
const waitOnUrl = (waitOn, waitOnTimeout = 60) => {
|
|
6161
|
+
console.log(
|
|
6162
|
+
'waiting on "%s" with timeout of %s seconds',
|
|
6163
|
+
waitOn,
|
|
6164
|
+
waitOnTimeout
|
|
6165
|
+
)
|
|
6166
|
+
|
|
6167
|
+
const waitTimeoutMs = waitOnTimeout * 1000
|
|
6168
|
+
|
|
6169
|
+
const waitUrls = waitOn
|
|
6170
|
+
.split(',')
|
|
6171
|
+
.map((s) => s.trim())
|
|
6172
|
+
.filter(Boolean)
|
|
6173
|
+
debug(`Waiting for urls ${waitUrls.join(', ')}`)
|
|
6174
|
+
|
|
6175
|
+
// run every wait promise after the previous has finished
|
|
6176
|
+
// to avoid "noise" of debug messages
|
|
6177
|
+
return waitUrls.reduce((prevPromise, url) => {
|
|
6178
|
+
return prevPromise.then(() => {
|
|
6179
|
+
debug(`Waiting for url ${url}`)
|
|
6180
|
+
return ping(url, waitTimeoutMs)
|
|
6181
|
+
})
|
|
6182
|
+
}, Promise.resolve())
|
|
6183
|
+
}
|
|
6184
|
+
|
|
6185
|
+
const waitOnMaybe = () => {
|
|
6186
|
+
const waitOn = core.getInput('wait-on')
|
|
6187
|
+
if (!waitOn) {
|
|
6188
|
+
return
|
|
6189
|
+
}
|
|
6190
|
+
|
|
6191
|
+
const waitOnTimeout = core.getInput('wait-on-timeout') || '60'
|
|
6192
|
+
const timeoutSeconds = parseFloat(waitOnTimeout)
|
|
6193
|
+
|
|
6194
|
+
if (isUrl(waitOn)) {
|
|
6195
|
+
return waitOnUrl(waitOn, timeoutSeconds)
|
|
6196
|
+
}
|
|
6197
|
+
|
|
6198
|
+
console.log('Waiting using command "%s"', waitOn)
|
|
6199
|
+
return execCommand(waitOn, true)
|
|
6200
|
+
}
|
|
6201
|
+
|
|
6202
|
+
const I = (x) => x
|
|
6203
|
+
|
|
6204
|
+
/**
|
|
6205
|
+
* Asks Cypress API if there were already builds for this commit.
|
|
6206
|
+
* In that case increments the count to get unique parallel id.
|
|
6207
|
+
*/
|
|
6208
|
+
const getCiBuildId = async () => {
|
|
6209
|
+
const {
|
|
6210
|
+
GITHUB_WORKFLOW,
|
|
6211
|
+
GITHUB_SHA,
|
|
6212
|
+
GITHUB_TOKEN,
|
|
6213
|
+
GITHUB_RUN_ID,
|
|
6214
|
+
GITHUB_REPOSITORY
|
|
6215
|
+
} = process.env
|
|
6216
|
+
|
|
6217
|
+
const [owner, repo] = GITHUB_REPOSITORY.split('/')
|
|
6218
|
+
let branch
|
|
6219
|
+
let buildId = `${GITHUB_WORKFLOW} - ${GITHUB_SHA}`
|
|
6220
|
+
|
|
6221
|
+
if (GITHUB_TOKEN) {
|
|
6222
|
+
debug(
|
|
6223
|
+
`Determining build id by asking GitHub about run ${GITHUB_RUN_ID}`
|
|
6224
|
+
)
|
|
6225
|
+
|
|
6226
|
+
const client = new Octokit({
|
|
6227
|
+
auth: GITHUB_TOKEN
|
|
6228
|
+
})
|
|
6229
|
+
|
|
6230
|
+
const resp = await client.request(
|
|
6231
|
+
'GET /repos/:owner/:repo/actions/runs/:run_id',
|
|
6232
|
+
{
|
|
6233
|
+
owner,
|
|
6234
|
+
repo,
|
|
6235
|
+
run_id: parseInt(GITHUB_RUN_ID)
|
|
6236
|
+
}
|
|
6237
|
+
)
|
|
6238
|
+
|
|
6239
|
+
if (resp && resp.data && resp.data.head_branch) {
|
|
6240
|
+
branch = resp.data.head_branch
|
|
6241
|
+
debug(`found the branch name ${branch}`)
|
|
6242
|
+
}
|
|
6243
|
+
|
|
6244
|
+
// This will return the complete list of jobs for a run with their steps,
|
|
6245
|
+
// this should always return data when there are jobs on the workflow.
|
|
6246
|
+
// Every time the workflow is re-run the jobs length should stay the same
|
|
6247
|
+
// (because the same amount of jobs were ran) but the id of them should change
|
|
6248
|
+
// letting us, select the first id as unique id
|
|
6249
|
+
// https://docs.github.com/en/rest/reference/actions#list-jobs-for-a-workflow-run
|
|
6250
|
+
const runsList = await client.request(
|
|
6251
|
+
'GET /repos/:owner/:repo/actions/runs/:run_id/jobs',
|
|
6252
|
+
{
|
|
6253
|
+
owner,
|
|
6254
|
+
repo,
|
|
6255
|
+
run_id: parseInt(GITHUB_RUN_ID)
|
|
6256
|
+
}
|
|
6257
|
+
)
|
|
6258
|
+
|
|
6259
|
+
if (
|
|
6260
|
+
runsList &&
|
|
6261
|
+
runsList.data &&
|
|
6262
|
+
runsList.data.jobs &&
|
|
6263
|
+
runsList.data.jobs.length
|
|
6264
|
+
) {
|
|
6265
|
+
const jobId = runsList.data.jobs[0].id
|
|
6266
|
+
debug(`fetched run list with jobId ${jobId}`)
|
|
6267
|
+
buildId = `${GITHUB_RUN_ID}-${jobId}`
|
|
6268
|
+
} else {
|
|
6269
|
+
debug('could not get run list data')
|
|
6270
|
+
}
|
|
6271
|
+
}
|
|
6272
|
+
|
|
6273
|
+
debug(`determined branch ${branch} and build id ${buildId}`)
|
|
6274
|
+
return { branch, buildId }
|
|
6275
|
+
}
|
|
6276
|
+
|
|
6277
|
+
/**
|
|
6278
|
+
* Forms entire command line like "npx cypress run ..."
|
|
6279
|
+
*/
|
|
6280
|
+
const runTestsUsingCommandLine = async () => {
|
|
6281
|
+
debug('Running Cypress tests using CLI command')
|
|
6282
|
+
const quoteArgument = isWindows() ? quoteWindowsArgument : I
|
|
6283
|
+
|
|
6284
|
+
const commandPrefix = core.getInput('command-prefix')
|
|
6285
|
+
if (!commandPrefix) {
|
|
6286
|
+
throw new Error('Expected command prefix')
|
|
6287
|
+
}
|
|
6288
|
+
|
|
6289
|
+
const record = getInputBool('record')
|
|
6290
|
+
const parallel = getInputBool('parallel')
|
|
6291
|
+
const headless = getInputBool('headless')
|
|
6292
|
+
|
|
6293
|
+
// TODO using yarn to run cypress when yarn is used for install
|
|
6294
|
+
// split potentially long command?
|
|
6295
|
+
|
|
6296
|
+
let cmd = []
|
|
6297
|
+
// we need to split the command prefix into individual arguments
|
|
6298
|
+
// otherwise they are passed all as a single string
|
|
6299
|
+
const parts = commandPrefix.split(' ')
|
|
6300
|
+
cmd = cmd.concat(parts)
|
|
6301
|
+
debug(`with concatenated command prefix: ${cmd.join(' ')}`)
|
|
6302
|
+
|
|
6303
|
+
// push each CLI argument separately
|
|
6304
|
+
cmd.push('cypress')
|
|
6305
|
+
cmd.push('run')
|
|
6306
|
+
if (headless) {
|
|
6307
|
+
cmd.push('--headless')
|
|
6308
|
+
}
|
|
6309
|
+
if (record) {
|
|
6310
|
+
cmd.push('--record')
|
|
6311
|
+
}
|
|
6312
|
+
if (parallel) {
|
|
6313
|
+
cmd.push('--parallel')
|
|
6314
|
+
}
|
|
6315
|
+
const group = core.getInput('group')
|
|
6316
|
+
if (group) {
|
|
6317
|
+
cmd.push('--group')
|
|
6318
|
+
cmd.push(quoteArgument(group))
|
|
6319
|
+
}
|
|
6320
|
+
const tag = core.getInput('tag')
|
|
6321
|
+
if (tag) {
|
|
6322
|
+
cmd.push('--tag')
|
|
6323
|
+
cmd.push(quoteArgument(tag))
|
|
6324
|
+
}
|
|
6325
|
+
const configInput = core.getInput('config')
|
|
6326
|
+
if (configInput) {
|
|
6327
|
+
cmd.push('--config')
|
|
6328
|
+
cmd.push(quoteArgument(configInput))
|
|
6329
|
+
}
|
|
6330
|
+
const spec = getSpecsList()
|
|
6331
|
+
if (spec) {
|
|
6332
|
+
cmd.push('--spec')
|
|
6333
|
+
cmd.push(quoteArgument(spec))
|
|
6334
|
+
}
|
|
6335
|
+
const project = core.getInput('project')
|
|
6336
|
+
if (project) {
|
|
6337
|
+
cmd.push('--project')
|
|
6338
|
+
cmd.push(quoteArgument(project))
|
|
6339
|
+
}
|
|
6340
|
+
const configFileInput = core.getInput('config-file')
|
|
6341
|
+
if (configFileInput) {
|
|
6342
|
+
cmd.push('--config-file')
|
|
6343
|
+
cmd.push(quoteArgument(configFileInput))
|
|
6344
|
+
}
|
|
6345
|
+
|
|
6346
|
+
if (parallel || group) {
|
|
6347
|
+
const { branch, buildId } = await getCiBuildId()
|
|
6348
|
+
if (branch) {
|
|
6349
|
+
core.exportVariable('GH_BRANCH', branch)
|
|
6350
|
+
}
|
|
6351
|
+
|
|
6352
|
+
cmd.push('--ci-build-id')
|
|
6353
|
+
const ciBuildId = core.getInput('ci-build-id') || buildId
|
|
6354
|
+
cmd.push(quoteArgument(ciBuildId))
|
|
6355
|
+
}
|
|
6356
|
+
|
|
6357
|
+
const browser = core.getInput('browser')
|
|
6358
|
+
if (browser) {
|
|
6359
|
+
cmd.push('--browser')
|
|
6360
|
+
// TODO should browser be quoted?
|
|
6361
|
+
// If it is a path, it might have spaces
|
|
6362
|
+
cmd.push(browser)
|
|
6363
|
+
}
|
|
6364
|
+
|
|
6365
|
+
const envInput = core.getInput('env')
|
|
6366
|
+
if (envInput) {
|
|
6367
|
+
// TODO should env be quoted?
|
|
6368
|
+
// If it is a JSON, it might have spaces
|
|
6369
|
+
cmd.push('--env')
|
|
6370
|
+
cmd.push(envInput)
|
|
6371
|
+
}
|
|
6372
|
+
|
|
6373
|
+
const quiet = getInputBool('quiet')
|
|
6374
|
+
if (quiet) {
|
|
6375
|
+
cmd.push('--quiet')
|
|
6376
|
+
}
|
|
6377
|
+
|
|
6378
|
+
console.log('Cypress test command: npx %s', cmd.join(' '))
|
|
6379
|
+
|
|
6380
|
+
// since we have quoted arguments ourselves, do not double quote them
|
|
6381
|
+
const opts = {
|
|
6382
|
+
...cypressCommandOptions,
|
|
6383
|
+
windowsVerbatimArguments: false
|
|
6384
|
+
}
|
|
6385
|
+
|
|
6386
|
+
debug(`in working directory "${cypressCommandOptions.cwd}"`)
|
|
6387
|
+
|
|
6388
|
+
const npxPath = await io.which('npx', true)
|
|
6389
|
+
debug(`npx path: ${npxPath}`)
|
|
6390
|
+
|
|
6391
|
+
return exec.exec(quote(npxPath), cmd, opts)
|
|
6392
|
+
}
|
|
6393
|
+
|
|
6394
|
+
/**
|
|
6395
|
+
* Run Cypress tests by collecting input parameters
|
|
6396
|
+
* and using Cypress module API to run tests.
|
|
6397
|
+
* @see https://on.cypress.io/module-api
|
|
6398
|
+
*/
|
|
6399
|
+
const runTests = async () => {
|
|
6400
|
+
const runTests = getInputBool('runTests', true)
|
|
6401
|
+
if (!runTests) {
|
|
6402
|
+
console.log('Skipping running tests: runTests parameter is false')
|
|
6403
|
+
return
|
|
6404
|
+
}
|
|
6405
|
+
|
|
6406
|
+
// export common environment variables that help run Cypress
|
|
6407
|
+
core.exportVariable('CYPRESS_CACHE_FOLDER', CYPRESS_CACHE_FOLDER)
|
|
6408
|
+
core.exportVariable('TERM', 'xterm')
|
|
6409
|
+
|
|
6410
|
+
const customCommand = core.getInput('command')
|
|
6411
|
+
if (customCommand) {
|
|
6412
|
+
console.log('Using custom test command: %s', customCommand)
|
|
6413
|
+
return execCommand(customCommand, true, 'run tests')
|
|
6414
|
+
}
|
|
6415
|
+
|
|
6416
|
+
const commandPrefix = core.getInput('command-prefix')
|
|
6417
|
+
if (commandPrefix) {
|
|
6418
|
+
return runTestsUsingCommandLine()
|
|
6419
|
+
}
|
|
6420
|
+
|
|
6421
|
+
debug('Running Cypress tests using NPM module API')
|
|
6422
|
+
debug(`requiring cypress dependency, cwd is ${process.cwd()}`)
|
|
6423
|
+
debug(`working directory ${workingDirectory}`)
|
|
6424
|
+
const cypressModulePath =
|
|
6425
|
+
require.resolve('cypress', {
|
|
6426
|
+
paths: [workingDirectory]
|
|
6427
|
+
}) || 'cypress'
|
|
6428
|
+
debug(`resolved cypress ${cypressModulePath}`)
|
|
6429
|
+
|
|
6430
|
+
const cypress = require(cypressModulePath)
|
|
6431
|
+
const cypressOptions = {
|
|
6432
|
+
headless: getInputBool('headless'),
|
|
6433
|
+
record: getInputBool('record'),
|
|
6434
|
+
parallel: getInputBool('parallel'),
|
|
6435
|
+
quiet: getInputBool('quiet')
|
|
6436
|
+
}
|
|
6437
|
+
|
|
6438
|
+
if (core.getInput('group')) {
|
|
6439
|
+
cypressOptions.group = core.getInput('group')
|
|
6440
|
+
}
|
|
6441
|
+
if (core.getInput('tag')) {
|
|
6442
|
+
cypressOptions.tag = core.getInput('tag')
|
|
6443
|
+
}
|
|
6444
|
+
if (core.getInput('config')) {
|
|
6445
|
+
cypressOptions.config = core.getInput('config')
|
|
6446
|
+
debug(`Cypress config "${cypressOptions.config}"`)
|
|
6447
|
+
}
|
|
6448
|
+
const spec = getSpecsList()
|
|
6449
|
+
if (spec) {
|
|
6450
|
+
cypressOptions.spec = spec
|
|
6451
|
+
}
|
|
6452
|
+
if (core.getInput('config-file')) {
|
|
6453
|
+
cypressOptions.configFile = core.getInput('config-file')
|
|
6454
|
+
}
|
|
6455
|
+
|
|
6456
|
+
// if the user set the explicit folder, use that
|
|
6457
|
+
if (core.getInput('project')) {
|
|
6458
|
+
cypressOptions.project = core.getInput('project')
|
|
6459
|
+
}
|
|
6460
|
+
if (core.getInput('browser')) {
|
|
6461
|
+
cypressOptions.browser = core.getInput('browser')
|
|
6462
|
+
}
|
|
6463
|
+
if (core.getInput('env')) {
|
|
6464
|
+
cypressOptions.env = core.getInput('env')
|
|
6465
|
+
}
|
|
6466
|
+
|
|
6467
|
+
if (cypressOptions.parallel || cypressOptions.group) {
|
|
6468
|
+
const { branch, buildId } = await getCiBuildId()
|
|
6469
|
+
if (branch) {
|
|
6470
|
+
core.exportVariable('GH_BRANCH', branch)
|
|
6471
|
+
}
|
|
6472
|
+
|
|
6473
|
+
cypressOptions.ciBuildId = core.getInput('ci-build-id') || buildId
|
|
6474
|
+
}
|
|
6475
|
+
|
|
6476
|
+
debug(`Cypress options ${JSON.stringify(cypressOptions)}`)
|
|
6477
|
+
|
|
6478
|
+
const onTestsFinished = (testResults) => {
|
|
6479
|
+
process.chdir(startWorkingDirectory)
|
|
6480
|
+
|
|
6481
|
+
if (testResults.failures) {
|
|
6482
|
+
console.error('Test run failed, code %d', testResults.failures)
|
|
6483
|
+
console.error('More information might be available above')
|
|
6484
|
+
|
|
6485
|
+
if (testResults.message) {
|
|
6486
|
+
console.error(
|
|
6487
|
+
'Cypress module has returned the following error message:'
|
|
6488
|
+
)
|
|
6489
|
+
console.error(testResults.message)
|
|
6490
|
+
}
|
|
6491
|
+
|
|
6492
|
+
return Promise.reject(
|
|
6493
|
+
new Error(testResults.message || 'Error running Cypress')
|
|
6494
|
+
)
|
|
6495
|
+
}
|
|
6496
|
+
|
|
6497
|
+
debug(`Cypress tests: ${testResults.totalFailed} failed`)
|
|
6498
|
+
|
|
6499
|
+
const dashboardUrl = testResults.runUrl
|
|
6500
|
+
if (dashboardUrl) {
|
|
6501
|
+
debug(`Dashboard url ${dashboardUrl}`)
|
|
6502
|
+
} else {
|
|
6503
|
+
debug('There is no Dashboard url')
|
|
6504
|
+
}
|
|
6505
|
+
// we still set the output explicitly
|
|
6506
|
+
core.setOutput('dashboardUrl', dashboardUrl)
|
|
6507
|
+
|
|
6508
|
+
if (testResults.totalFailed) {
|
|
6509
|
+
return Promise.reject(
|
|
6510
|
+
new Error(`Cypress tests: ${testResults.totalFailed} failed`)
|
|
6511
|
+
)
|
|
6512
|
+
}
|
|
6513
|
+
}
|
|
6514
|
+
|
|
6515
|
+
const onTestsError = (e) => {
|
|
6516
|
+
process.chdir(startWorkingDirectory)
|
|
6517
|
+
|
|
6518
|
+
console.error(e)
|
|
6519
|
+
return Promise.reject(e)
|
|
6520
|
+
}
|
|
6521
|
+
|
|
6522
|
+
process.chdir(workingDirectory)
|
|
6523
|
+
return cypress
|
|
6524
|
+
.run(cypressOptions)
|
|
6525
|
+
.then(onTestsFinished, onTestsError)
|
|
6526
|
+
}
|
|
6527
|
+
|
|
6528
|
+
const installMaybe = () => {
|
|
6529
|
+
const installParameter = getInputBool('install', true)
|
|
6530
|
+
if (!installParameter) {
|
|
6531
|
+
console.log('Skipping install because install parameter is false')
|
|
6532
|
+
return Promise.resolve()
|
|
6533
|
+
}
|
|
6534
|
+
|
|
6535
|
+
return Promise.all([
|
|
6536
|
+
restoreCachedNpm(),
|
|
6537
|
+
restoreCachedCypressBinary()
|
|
6538
|
+
]).then(([npmCacheHit, cypressCacheHit]) => {
|
|
6539
|
+
debug(`npm cache hit ${npmCacheHit}`)
|
|
6540
|
+
debug(`cypress cache hit ${cypressCacheHit}`)
|
|
6541
|
+
|
|
6542
|
+
return install().then(() => {
|
|
6543
|
+
debug('install has finished')
|
|
6544
|
+
return listCypressBinaries().then(() => {
|
|
6545
|
+
if (npmCacheHit && cypressCacheHit) {
|
|
6546
|
+
debug('no need to verify Cypress binary or save caches')
|
|
6547
|
+
return Promise.resolve(undefined)
|
|
6548
|
+
}
|
|
6549
|
+
|
|
6550
|
+
debug('verifying Cypress binary')
|
|
6551
|
+
return verifyCypressBinary()
|
|
6552
|
+
.then(saveCachedNpm)
|
|
6553
|
+
.then(saveCachedCypressBinary)
|
|
6554
|
+
})
|
|
6555
|
+
})
|
|
6556
|
+
})
|
|
6557
|
+
}
|
|
6558
|
+
|
|
6559
|
+
installMaybe()
|
|
6560
|
+
.then(buildAppMaybe)
|
|
6561
|
+
.then(startServersMaybe)
|
|
6562
|
+
.then(waitOnMaybe)
|
|
6563
|
+
.then(runTests)
|
|
6564
|
+
.then(() => {
|
|
6565
|
+
debug('all done, exiting')
|
|
6566
|
+
// force exit to avoid waiting for child processes,
|
|
6567
|
+
// like the server we have started
|
|
6568
|
+
// see https://github.com/actions/toolkit/issues/216
|
|
6569
|
+
process.exit(0)
|
|
6570
|
+
})
|
|
6571
|
+
.catch((error) => {
|
|
6572
|
+
// final catch - when anything goes wrong, throw an error
|
|
6573
|
+
// and exit the action with non-zero code
|
|
6574
|
+
debug(error.message)
|
|
6575
|
+
debug(error.stack)
|
|
6576
|
+
|
|
6577
|
+
core.setFailed(error.message)
|
|
6578
|
+
process.exit(1)
|
|
6579
|
+
})
|
|
6580
6580
|
|
|
6581
6581
|
|
|
6582
6582
|
/***/ }),
|
|
@@ -66123,86 +66123,86 @@ function terminator(callback)
|
|
|
66123
66123
|
/* 946 */
|
|
66124
66124
|
/***/ (function(module, __unusedexports, __webpack_require__) {
|
|
66125
66125
|
|
|
66126
|
-
const core = __webpack_require__(470)
|
|
66127
|
-
const got = __webpack_require__(77)
|
|
66128
|
-
const debug = __webpack_require__(784)('@cypress/github-action')
|
|
66129
|
-
|
|
66130
|
-
/**
|
|
66131
|
-
* A small utility for checking when an URL responds, kind of
|
|
66132
|
-
* a poor man's https://www.npmjs.com/package/wait-on. This version
|
|
66133
|
-
* is implemented using https://github.com/sindresorhus/got
|
|
66134
|
-
*/
|
|
66135
|
-
const ping = (url, timeout) => {
|
|
66136
|
-
if (!timeout) {
|
|
66137
|
-
throw new Error('Expected timeout in ms')
|
|
66138
|
-
}
|
|
66139
|
-
|
|
66140
|
-
// make copy of the error codes that "got" retries on
|
|
66141
|
-
const errorCodes = [...got.defaults.options.retry.errorCodes]
|
|
66142
|
-
errorCodes.push('ESOCKETTIMEDOUT')
|
|
66143
|
-
|
|
66144
|
-
// we expect the server to respond within a time limit
|
|
66145
|
-
// and if it does not - retry up to total "timeout" duration
|
|
66146
|
-
const individualPingTimeout = Math.min(timeout, 30000)
|
|
66147
|
-
|
|
66148
|
-
// add to the timeout max individual ping timeout
|
|
66149
|
-
// to avoid long-waiting ping from "rolling" over the end
|
|
66150
|
-
// and preventing pinging the last time
|
|
66151
|
-
timeout += individualPingTimeout
|
|
66152
|
-
const limit = Math.ceil(timeout / individualPingTimeout)
|
|
66153
|
-
|
|
66154
|
-
debug(`total ping timeout ${timeout}`)
|
|
66155
|
-
debug(`individual ping timeout ${individualPingTimeout}ms`)
|
|
66156
|
-
debug(`retries limit ${limit}`)
|
|
66157
|
-
|
|
66158
|
-
const start = +new Date()
|
|
66159
|
-
return got(url, {
|
|
66160
|
-
headers: {
|
|
66161
|
-
Accept: 'text/html, application/json, text/plain, */*'
|
|
66162
|
-
},
|
|
66163
|
-
timeout: individualPingTimeout,
|
|
66164
|
-
errorCodes,
|
|
66165
|
-
retry: {
|
|
66166
|
-
limit,
|
|
66167
|
-
calculateDelay({ error, attemptCount }) {
|
|
66168
|
-
if (error) {
|
|
66169
|
-
debug(`got error ${JSON.stringify(error)}`)
|
|
66170
|
-
}
|
|
66171
|
-
const now = +new Date()
|
|
66172
|
-
const elapsed = now - start
|
|
66173
|
-
debug(
|
|
66174
|
-
`${elapsed}ms ${error.method} ${error.host} ${error.code} attempt ${attemptCount}`
|
|
66175
|
-
)
|
|
66176
|
-
if (elapsed > timeout) {
|
|
66177
|
-
console.error(
|
|
66178
|
-
'%s timed out on retry %d of %d, elapsed %dms, limit %dms',
|
|
66179
|
-
url,
|
|
66180
|
-
attemptCount,
|
|
66181
|
-
limit,
|
|
66182
|
-
elapsed,
|
|
66183
|
-
timeout
|
|
66184
|
-
)
|
|
66185
|
-
return 0
|
|
66186
|
-
}
|
|
66187
|
-
|
|
66188
|
-
// if the error code is ECONNREFUSED use shorter timeout
|
|
66189
|
-
// because the server is probably starting
|
|
66190
|
-
if (error.code === 'ECONNREFUSED') {
|
|
66191
|
-
return 1000
|
|
66192
|
-
}
|
|
66193
|
-
|
|
66194
|
-
// default "long" timeout
|
|
66195
|
-
return individualPingTimeout
|
|
66196
|
-
}
|
|
66197
|
-
}
|
|
66198
|
-
}).then(() => {
|
|
66199
|
-
const now = +new Date()
|
|
66200
|
-
const elapsed = now - start
|
|
66201
|
-
debug(`pinging ${url} has finished ok after ${elapsed}ms`)
|
|
66202
|
-
})
|
|
66203
|
-
}
|
|
66204
|
-
|
|
66205
|
-
module.exports = { ping }
|
|
66126
|
+
const core = __webpack_require__(470)
|
|
66127
|
+
const got = __webpack_require__(77)
|
|
66128
|
+
const debug = __webpack_require__(784)('@cypress/github-action')
|
|
66129
|
+
|
|
66130
|
+
/**
|
|
66131
|
+
* A small utility for checking when an URL responds, kind of
|
|
66132
|
+
* a poor man's https://www.npmjs.com/package/wait-on. This version
|
|
66133
|
+
* is implemented using https://github.com/sindresorhus/got
|
|
66134
|
+
*/
|
|
66135
|
+
const ping = (url, timeout) => {
|
|
66136
|
+
if (!timeout) {
|
|
66137
|
+
throw new Error('Expected timeout in ms')
|
|
66138
|
+
}
|
|
66139
|
+
|
|
66140
|
+
// make copy of the error codes that "got" retries on
|
|
66141
|
+
const errorCodes = [...got.defaults.options.retry.errorCodes]
|
|
66142
|
+
errorCodes.push('ESOCKETTIMEDOUT')
|
|
66143
|
+
|
|
66144
|
+
// we expect the server to respond within a time limit
|
|
66145
|
+
// and if it does not - retry up to total "timeout" duration
|
|
66146
|
+
const individualPingTimeout = Math.min(timeout, 30000)
|
|
66147
|
+
|
|
66148
|
+
// add to the timeout max individual ping timeout
|
|
66149
|
+
// to avoid long-waiting ping from "rolling" over the end
|
|
66150
|
+
// and preventing pinging the last time
|
|
66151
|
+
timeout += individualPingTimeout
|
|
66152
|
+
const limit = Math.ceil(timeout / individualPingTimeout)
|
|
66153
|
+
|
|
66154
|
+
debug(`total ping timeout ${timeout}`)
|
|
66155
|
+
debug(`individual ping timeout ${individualPingTimeout}ms`)
|
|
66156
|
+
debug(`retries limit ${limit}`)
|
|
66157
|
+
|
|
66158
|
+
const start = +new Date()
|
|
66159
|
+
return got(url, {
|
|
66160
|
+
headers: {
|
|
66161
|
+
Accept: 'text/html, application/json, text/plain, */*'
|
|
66162
|
+
},
|
|
66163
|
+
timeout: individualPingTimeout,
|
|
66164
|
+
errorCodes,
|
|
66165
|
+
retry: {
|
|
66166
|
+
limit,
|
|
66167
|
+
calculateDelay({ error, attemptCount }) {
|
|
66168
|
+
if (error) {
|
|
66169
|
+
debug(`got error ${JSON.stringify(error)}`)
|
|
66170
|
+
}
|
|
66171
|
+
const now = +new Date()
|
|
66172
|
+
const elapsed = now - start
|
|
66173
|
+
debug(
|
|
66174
|
+
`${elapsed}ms ${error.method} ${error.host} ${error.code} attempt ${attemptCount}`
|
|
66175
|
+
)
|
|
66176
|
+
if (elapsed > timeout) {
|
|
66177
|
+
console.error(
|
|
66178
|
+
'%s timed out on retry %d of %d, elapsed %dms, limit %dms',
|
|
66179
|
+
url,
|
|
66180
|
+
attemptCount,
|
|
66181
|
+
limit,
|
|
66182
|
+
elapsed,
|
|
66183
|
+
timeout
|
|
66184
|
+
)
|
|
66185
|
+
return 0
|
|
66186
|
+
}
|
|
66187
|
+
|
|
66188
|
+
// if the error code is ECONNREFUSED use shorter timeout
|
|
66189
|
+
// because the server is probably starting
|
|
66190
|
+
if (error.code === 'ECONNREFUSED') {
|
|
66191
|
+
return 1000
|
|
66192
|
+
}
|
|
66193
|
+
|
|
66194
|
+
// default "long" timeout
|
|
66195
|
+
return individualPingTimeout
|
|
66196
|
+
}
|
|
66197
|
+
}
|
|
66198
|
+
}).then(() => {
|
|
66199
|
+
const now = +new Date()
|
|
66200
|
+
const elapsed = now - start
|
|
66201
|
+
debug(`pinging ${url} has finished ok after ${elapsed}ms`)
|
|
66202
|
+
})
|
|
66203
|
+
}
|
|
66204
|
+
|
|
66205
|
+
module.exports = { ping }
|
|
66206
66206
|
|
|
66207
66207
|
|
|
66208
66208
|
/***/ }),
|