index.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. 'use strict';
  2. const helper = require('../helper');
  3. /**
  4. * Default arguments for the `--ignore` option.
  5. * @type {string[]}
  6. */
  7. const DEFAULT_IGNORE = ['node_modules'];
  8. /**
  9. * Schema for the `upload-sourcemaps` command.
  10. * @type {OptionsSchema}
  11. */
  12. const SOURCEMAPS_SCHEMA = require('./options/uploadSourcemaps');
  13. /**
  14. * Schema for the `deploys new` command.
  15. * @type {OptionsSchema}
  16. */
  17. const DEPLOYS_SCHEMA = require('./options/deploys');
  18. /**
  19. * Manages releases and release artifacts on Sentry.
  20. * @namespace SentryReleases
  21. */
  22. class Releases {
  23. /**
  24. * Creates a new `Releases` instance.
  25. *
  26. * @param {Object} [options] More options to pass to the CLI
  27. */
  28. constructor(options) {
  29. this.options = options || {};
  30. if (typeof this.options.configFile === 'string') {
  31. this.configFile = this.options.configFile;
  32. }
  33. delete this.options.configFile;
  34. }
  35. /**
  36. * Registers a new release with sentry.
  37. *
  38. * The given release name should be unique and deterministic. It can later be used to
  39. * upload artifacts, such as source maps.
  40. *
  41. * @param {string} release Unique name of the new release.
  42. * @param {object} options A set of options when creating a release.
  43. * @param {array} options.projects The list of project slugs for a release.
  44. * @returns {Promise} A promise that resolves when the release has been created.
  45. * @memberof SentryReleases
  46. */
  47. new(release, options) {
  48. const args = ['releases', 'new', release].concat(helper.getProjectFlagsFromOptions(options));
  49. return this.execute(args, null);
  50. }
  51. /**
  52. * Specifies the set of commits covered in this release.
  53. *
  54. * @param {string} release Unique name of the release
  55. * @param {object} options A set of options to configure the commits to include
  56. * @param {string} options.repo The full repo name as defined in Sentry
  57. * @param {boolean} options.auto Automatically choose the associated commit (uses
  58. * the current commit). Overrides other options.
  59. * @param {string} options.commit The current (last) commit in the release.
  60. * @param {string} options.previousCommit The commit before the beginning of this
  61. * release (in other words, the last commit of the previous release). If omitted,
  62. * this will default to the last commit of the previous release in Sentry. If there
  63. * was no previous release, the last 10 commits will be used.
  64. * @param {boolean} options.ignoreMissing When the flag is set and the previous release
  65. * commit was not found in the repository, will create a release with the default commits
  66. * count (or the one specified with `--initial-depth`) instead of failing the command.
  67. * @param {boolean} options.ignoreEmpty When the flag is set, command will not fail
  68. * and just exit silently if no new commits for a given release have been found.
  69. * @returns {Promise} A promise that resolves when the commits have been associated
  70. * @memberof SentryReleases
  71. */
  72. setCommits(release, options) {
  73. if (!options || (!options.auto && (!options.repo || !options.commit))) {
  74. throw new Error('options.auto, or options.repo and options.commit must be specified');
  75. }
  76. let commitFlags = [];
  77. if (options.auto) {
  78. commitFlags = ['--auto'];
  79. } else if (options.previousCommit) {
  80. commitFlags = ['--commit', `${options.repo}@${options.previousCommit}..${options.commit}`];
  81. } else {
  82. commitFlags = ['--commit', `${options.repo}@${options.commit}`];
  83. }
  84. if (options.ignoreMissing) {
  85. commitFlags.push('--ignore-missing');
  86. }
  87. if (options.ignoreEmpty) {
  88. commitFlags.push('--ignore-empty');
  89. }
  90. return this.execute(['releases', 'set-commits', release].concat(commitFlags));
  91. }
  92. /**
  93. * Marks this release as complete. This should be called once all artifacts has been
  94. * uploaded.
  95. *
  96. * @param {string} release Unique name of the release.
  97. * @returns {Promise} A promise that resolves when the release has been finalized.
  98. * @memberof SentryReleases
  99. */
  100. finalize(release) {
  101. return this.execute(['releases', 'finalize', release], null);
  102. }
  103. /**
  104. * Creates a unique, deterministic version identifier based on the project type and
  105. * source files. This identifier can be used as release name.
  106. *
  107. * @returns {Promise.<string>} A promise that resolves to the version string.
  108. * @memberof SentryReleases
  109. */
  110. proposeVersion() {
  111. return this.execute(['releases', 'propose-version'], null).then(
  112. version => version && version.trim()
  113. );
  114. }
  115. /**
  116. * Scans the given include folders for JavaScript source maps and uploads them to the
  117. * specified release for processing.
  118. *
  119. * The options require an `include` array, which is a list of directories to scan.
  120. * Additionally, it supports to ignore certain files, validate and preprocess source
  121. * maps and define a URL prefix.
  122. *
  123. * @example
  124. * await cli.releases.uploadSourceMaps(cli.releases.proposeVersion(), {
  125. * // required options:
  126. * include: ['build'],
  127. *
  128. * // default options:
  129. * ignore: ['node_modules'], // globs for files to ignore
  130. * ignoreFile: null, // path to a file with ignore rules
  131. * rewrite: false, // preprocess sourcemaps before uploading
  132. * sourceMapReference: true, // add a source map reference to source files
  133. * stripPrefix: [], // remove certain prefices from filenames
  134. * stripCommonPrefix: false, // guess common prefices to remove from filenames
  135. * validate: false, // validate source maps and cancel the upload on error
  136. * urlPrefix: '', // add a prefix source map urls after stripping them
  137. * urlSuffix: '', // add a suffix source map urls after stripping them
  138. * ext: ['js', 'map', 'jsbundle', 'bundle'], // override file extensions to scan for
  139. * projects: ['node'] // provide a list of projects
  140. * });
  141. *
  142. * @param {string} release Unique name of the release.
  143. * @param {object} options Options to configure the source map upload.
  144. * @returns {Promise} A promise that resolves when the upload has completed successfully.
  145. * @memberof SentryReleases
  146. */
  147. uploadSourceMaps(release, options) {
  148. if (!options || !options.include || !Array.isArray(options.include)) {
  149. throw new Error(
  150. '`options.include` must be a vaild array of paths and/or path descriptor objects.'
  151. );
  152. }
  153. // Each entry in the `include` array will map to an array of promises, which
  154. // will in turn contain one promise per literal path value. Thus `uploads`
  155. // will be an array of Promise arrays, which we'll flatten later.
  156. const uploads = options.include.map(includeEntry => {
  157. let pathOptions;
  158. let uploadPaths;
  159. if (typeof includeEntry === 'object') {
  160. pathOptions = includeEntry;
  161. uploadPaths = includeEntry.paths;
  162. if (!Array.isArray(uploadPaths)) {
  163. throw new Error(
  164. `Path descriptor objects in \`options.include\` must contain a \`paths\` array. Got ${includeEntry}.`
  165. );
  166. }
  167. }
  168. // `includeEntry` should be a string, which we can wrap in an array to
  169. // match the `paths` property in the descriptor object type
  170. else {
  171. pathOptions = {};
  172. uploadPaths = [includeEntry];
  173. }
  174. const newOptions = { ...options, ...pathOptions };
  175. if (!newOptions.ignoreFile && !newOptions.ignore) {
  176. newOptions.ignore = DEFAULT_IGNORE;
  177. }
  178. // args which apply to the entire `include` entry (everything besides the path)
  179. const args = ['releases']
  180. .concat(helper.getProjectFlagsFromOptions(options))
  181. .concat(['files', release, 'upload-sourcemaps']);
  182. return uploadPaths.map(path =>
  183. // `execute()` is async and thus we're returning a promise here
  184. this.execute(helper.prepareCommand([...args, path], SOURCEMAPS_SCHEMA, newOptions), true)
  185. );
  186. });
  187. // `uploads` is an array of Promise arrays, which needs to be flattened
  188. // before being passed to `Promise.all()`. (`Array.flat()` doesn't exist in
  189. // Node < 11; this polyfill takes advantage of the fact that `concat()` is
  190. // willing to accept an arbitrary number of items to add to and/or iterables
  191. // to unpack into the given array.)
  192. return Promise.all([].concat(...uploads));
  193. }
  194. /**
  195. * List all deploys for a given release.
  196. *
  197. * @param {string} release Unique name of the release.
  198. * @returns {Promise} A promise that resolves when the list comes back from the server.
  199. * @memberof SentryReleases
  200. */
  201. listDeploys(release) {
  202. return this.execute(['releases', 'deploys', release, 'list'], null);
  203. }
  204. /**
  205. * Creates a new release deployment. This should be called after the release has been
  206. * finalized, while deploying on a given environment.
  207. *
  208. * @example
  209. * await cli.releases.newDeploy(cli.releases.proposeVersion(), {
  210. * // required options:
  211. * env: 'production', // environment for this release. Values that make sense here would be 'production' or 'staging'
  212. *
  213. * // optional options:
  214. * started: 42, // unix timestamp when the deployment started
  215. * finished: 1337, // unix timestamp when the deployment finished
  216. * time: 1295, // deployment duration in seconds. This can be specified alternatively to `started` and `finished`
  217. * name: 'PickleRick', // human readable name for this deployment
  218. * url: 'https://example.com', // URL that points to the deployment
  219. * });
  220. *
  221. * @param {string} release Unique name of the release.
  222. * @param {object} options Options to configure the new release deploy.
  223. * @returns {Promise} A promise that resolves when the deploy has been created.
  224. * @memberof SentryReleases
  225. */
  226. newDeploy(release, options) {
  227. if (!options || !options.env) {
  228. throw new Error('options.env must be a vaild name');
  229. }
  230. const args = ['releases', 'deploys', release, 'new'];
  231. return this.execute(helper.prepareCommand(args, DEPLOYS_SCHEMA, options), null);
  232. }
  233. /**
  234. * See {helper.execute} docs.
  235. * @param {string[]} args Command line arguments passed to `sentry-cli`.
  236. * @param {boolean} live We inherit stdio to display `sentry-cli` output directly.
  237. * @returns {Promise.<string>} A promise that resolves to the standard output.
  238. */
  239. execute(args, live) {
  240. return helper.execute(args, live, this.options.silent, this.configFile, this.options);
  241. }
  242. }
  243. module.exports = Releases;