Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

automatically unzip before upload, re-zip any zipped files after Sentry artifact upload #42

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 3 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
# Ember-cli-deploy-sentry [![Circle CI](https://circleci.com/gh/dschmidt/ember-cli-deploy-sentry/tree/master.svg?style=shield)](https://circleci.com/gh/dschmidt/ember-cli-deploy-sentry/tree/master)
# @jasonmit/ember-cli-deploy-sentry

> An ember-cli-deploy-plugin to upload javascript sourcemaps to [Sentry][1].

[![](https://ember-cli-deploy.github.io/ember-cli-deploy-version-badges/plugins/ember-cli-deploy-sentry.svg)](http://ember-cli-deploy.github.io/ember-cli-deploy-version-badges/)
This fork automatically unzips artifact files before uploading them to Sentry and re-zipping after the upload is finished. This is necessary since Sentry does not yet support gzipped artifacts.

## What is an ember-cli-deploy plugin?

A plugin is an addon that can be executed as a part of the ember-cli-deploy pipeline. A plugin will implement one or more of the ember-cli-deploy's pipeline hooks.

For more information on what plugins are and how they work, please refer to the [Plugin Documentation][10].
`ember i @jasonmit/ember-cli-deploy-sentry`

## Quick Start
To get up and running quickly, do the following:
Expand Down
Empty file removed addon/.gitkeep
Empty file.
256 changes: 158 additions & 98 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,64 +1,103 @@
/* jshint node: true */
'use strict';

var RSVP = require('rsvp');
var DeployPluginBase = require('ember-cli-deploy-plugin');
var SilentError = require('silent-error');
var glob = require("glob");
var urljoin = require("url-join");
var request = require('request-promise');
var path = require('path');
var fs = require('fs');
var throat = require('throat');
'use strict';

const fs = require('fs');
const zlib = require('zlib');
const RSVP = require('rsvp');
const glob = require('glob');
const path = require('path');
const throat = require('throat');
const isGzip = require('is-gzip');
const urljoin = require('url-join');
const request = require('request-promise');
const SilentError = require('silent-error');
const DeployPluginBase = require('ember-cli-deploy-plugin');

module.exports = {
name: 'ember-cli-deploy-sentry',

contentFor: function(type/*, config*/) {
contentFor(type /*, config*/) {
if (type === 'head-footer') {
return '<meta name="sentry:revision">';
}
},

createDeployPlugin: function(options) {
var DeployPlugin = DeployPluginBase.extend({
createDeployPlugin(options) {
let DeployPlugin = DeployPluginBase.extend({
name: options.name,
runAfter: ['gzip', 's3'],
requiredConfig: [
'publicUrl',
'sentryUrl',
'sentryOrganizationSlug',
'sentryProjectSlug',
'revisionKey'
],

defaultConfig: {
distDir: function(context) {
return context.distDir;
},
filePattern: '/**/*.{js,map}',
revisionKey: function(context) {
return context.revisionData && context.revisionData.revisionKey;
},
enableRevisionTagging: true,
replaceFiles: true,
enableRevisionTagging: true,
replaceFiles: true,
strictSSL: true,
distDir(context) {
return context.distDir;
},
revisionKey(context) {
return context.revisionData && context.revisionData.revisionKey;
}
},
requiredConfig: ['publicUrl', 'sentryUrl', 'sentryOrganizationSlug', 'sentryProjectSlug', 'revisionKey'],

prepare: function(context) {
var isEnabled = this.readConfig('enableRevisionTagging');
if(!isEnabled) {
prepare(context) {
let isEnabled = this.readConfig('enableRevisionTagging');

if (!isEnabled) {
return;
}

var revisionKey = this.readConfig('revisionKey');
if(!revisionKey) {
return new SilentError("Could not find revision key to fingerprint Sentry revision with.");
let revisionKey = this.readConfig('revisionKey');
if (!revisionKey) {
return new SilentError('Could not find revision key to fingerprint Sentry revision with.');
}

// TODO instead of plainly reading index.html, minimatch
// getConfig('revision patterns') on context.distFiles
var indexPath = path.join(context.distDir, "index.html");
var index = fs.readFileSync(indexPath, 'utf8');
index = index.replace('<meta name="sentry:revision">',
'<meta name="sentry:revision" content="'+revisionKey+'">');
let indexPath = path.join(context.distDir, 'index.html');
let index = fs.readFileSync(indexPath, 'utf8');
index = index.replace(
'<meta name="sentry:revision">',
'<meta name="sentry:revision" content="' + revisionKey + '">'
);
fs.writeFileSync(indexPath, index);
},

upload: function(/* context */) {
upload(/* context */) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method is all that really changed, focus the review here. Everything else is formatting and will be discarded when I open up the "official" PR if there is interest.

let dir = this.readConfig('distDir');
let filePattern = this.readConfig('filePattern');
let pattern = path.join(dir, filePattern);
let files = glob.sync(pattern);

let zipped = files
.map(file => {
return {
file,
buffer: fs.readFileSync(file)
};
})
.filter(({ file, buffer }) => {
if (isGzip(buffer)) {
this.log(`un-gzipping ${file}`);
fs.writeFileSync(file, zlib.gunzipSync(buffer));
this.log(`✔ un-gzipped ${file}`);

return true;
}

return false;
});

this.sentrySettings = {
url: this.readConfig('sentryUrl'),
publicUrl: this.readConfig('publicUrl'),
Expand All @@ -68,53 +107,75 @@ module.exports = {
bearerApiKey: this.readConfig('sentryBearerApiKey'),
release: this.readConfig('revisionKey')
};
this.baseUrl = urljoin(this.sentrySettings.url, '/api/0/projects/', this.sentrySettings.organizationSlug, this.sentrySettings.projectSlug, '/releases/');

this.baseUrl = urljoin(
this.sentrySettings.url,
'/api/0/projects/',
this.sentrySettings.organizationSlug,
this.sentrySettings.projectSlug,
'/releases/'
);

this.releaseUrl = urljoin(this.baseUrl, this.sentrySettings.release, '/');

if(!this.sentrySettings.release) {
throw new SilentError('revisionKey setting is not available, either provide it manually or make sure the ember-cli-deploy-revision-data plugin is loaded');
if (!this.sentrySettings.release) {
throw new SilentError(
'revisionKey setting is not available, either provide it manually or make sure the ember-cli-deploy-revision-data plugin is loaded'
);
}

let rezip = () => {
zipped.forEach(({ file, buffer }) => {
this.log(`restoring original ${file} contents`);
fs.writeFileSync(file, buffer);
this.log(`✔ restored ${file}`);
});
};

return this.doesReleaseExist(this.releaseUrl)
.then(this.handleExistingRelease.bind(this))
.catch(this.createRelease.bind(this));
.then(() => this.handleExistingRelease())
.then(() => rezip())
.catch(err => this.createRelease(err).then(() => rezip()));
},

generateAuth: function() {
var apiKey = this.sentrySettings.apiKey;
var bearerApiKey = this.sentrySettings.bearerApiKey;
generateAuth() {
let apiKey = this.sentrySettings.apiKey;
let bearerApiKey = this.sentrySettings.bearerApiKey;
if (bearerApiKey !== undefined) {
return { bearer: bearerApiKey };
}
return { user: apiKey };
},

doesReleaseExist: function(releaseUrl) {
doesReleaseExist(releaseUrl) {
return request({
uri: releaseUrl,
auth: this.generateAuth(),
json: true,
strictSSL: this.readConfig('strictSSL'),
strictSSL: this.readConfig('strictSSL')
});
},
handleExistingRelease: function handleExistingRelease(response) {
this.log('Release ' + response.version + ' exists.', {verbose: true});
this.log('Retrieving release files.', {verbose: true});
return this._getReleaseFiles().then(function(response) {
if (this.readConfig('replaceFiles')) {
this.log('Replacing files.', {verbose: true});
return RSVP.all(response.map(this._deleteFile, this))
.then(this._doUpload.bind(this))
.then(this._logFiles.bind(this, response));
} else {
this.log('Leaving files alone.', {verbose: true});
return this._logFiles(response);
}
}.bind(this));

handleExistingRelease(response) {
this.log('Release ' + response.version + ' exists.', { verbose: true });
this.log('Retrieving release files.', { verbose: true });
return this._getReleaseFiles().then(
function(response) {
if (this.readConfig('replaceFiles')) {
this.log('Replacing files.', { verbose: true });
return RSVP.all(response.map(this._deleteFile, this))
.then(this._doUpload.bind(this))
.then(this._logFiles.bind(this, response));
} else {
this.log('Leaving files alone.', { verbose: true });
return this._logFiles(response);
}
}.bind(this)
);
},
createRelease: function createRelease(error) {
createRelease(error) {
if (error.statusCode === 404) {
this.log('Release does not exist. Creating.', {verbose: true});
this.log('Release does not exist. Creating.', { verbose: true });
} else if (error.statusCode === 400) {
this.log('Bad Request. Not Continuing');
return RSVP.resolve(error.message);
Expand All @@ -131,26 +192,25 @@ module.exports = {
resolveWithFullResponse: true,
strictSSL: this.readConfig('strictSSL'),
})
.then(this._doUpload.bind(this))
.then(this._logFiles.bind(this))
.catch(function(err){
console.error(err);
throw new SilentError('Creating release failed');
});
.then(this._doUpload.bind(this))
.then(this._logFiles.bind(this))
.catch(function(err) {
console.error(err);
throw new SilentError('Creating release failed');
});
},
_doUpload: function doUpload() {
return this._getFilesToUpload()
.then(this._uploadFileList.bind(this));
_doUpload() {
return this._getFilesToUpload().then(this._uploadFileList.bind(this));
},
_getFilesToUpload: function getFilesToUpload() {
this.log('Generating file list for upload', {verbose: true});
var dir = this.readConfig('distDir');
var filePattern = this.readConfig('filePattern');
var pattern = path.join(dir, filePattern);
_getFilesToUpload() {
this.log('Generating file list for upload', { verbose: true });
let dir = this.readConfig('distDir');
let filePattern = this.readConfig('filePattern');
let pattern = path.join(dir, filePattern);
return new RSVP.Promise(function(resolve, reject) {
// options is optional
glob(pattern, function (err, files) {
if(err) {
glob(pattern, function(err, files) {
if (err) {
reject(err);
} else {
resolve(files);
Expand All @@ -162,18 +222,17 @@ module.exports = {
});
});
},
_uploadFileList: function uploadFileList(files) {
this.log('Beginning upload.', {verbose: true});
return RSVP.all(files.map(throat(5, this._uploadFile.bind(this))))
.then(this._getReleaseFiles.bind(this));
_uploadFileList(files) {
this.log('Beginning upload.', { verbose: true });
return RSVP.all(files.map(throat(5, this._uploadFile.bind(this)))).then(this._getReleaseFiles.bind(this));
},
_uploadFile: function uploadFile(filePath) {
var distDir = this.readConfig('distDir');
var fileName = path.join(distDir, filePath);
_uploadFile(filePath) {
let distDir = this.readConfig('distDir');
let fileName = path.join(distDir, filePath);

var formData = {
let formData = {
name: urljoin(this.sentrySettings.publicUrl, filePath),
file: fs.createReadStream(fileName),
file: fs.createReadStream(fileName)
};

return request({
Expand All @@ -184,38 +243,39 @@ module.exports = {
strictSSL: this.readConfig('strictSSL'),
});
},
_getReleaseFiles: function getReleaseFiles() {
_getReleaseFiles() {
return request({
uri: urljoin(this.releaseUrl, 'files/'),
auth: this.generateAuth(),
json: true,
strictSSL: this.readConfig('strictSSL'),
});
},
_deleteFile: function deleteFile(file) {
this.log('Deleting ' + file.name, {verbose: true});
_deleteFile(file) {
this.log('Deleting ' + file.name, { verbose: true });
return request({
uri: urljoin(this.releaseUrl, 'files/', file.id, '/'),
method: 'DELETE',
auth: this.generateAuth(),
strictSSL: this.readConfig('strictSSL'),
strictSSL: this.readConfig('strictSSL')
});
},
_logFiles: function logFiles(response) {
_logFiles(response) {
this.log('Files known to sentry for this release', { verbose: true });
response.forEach(function(file) { this.log('✔ ' + file.name, { verbose: true }); }, this);
response.forEach(file => this.log('✔ ' + file.name, { verbose: true }));
},

didDeploy: function(/* context */){
var deployMessage = "Uploaded sourcemaps to sentry release: "
+ this.readConfig('sentryUrl')
+ '/'
+ this.readConfig('sentryOrganizationSlug')
+ '/'
+ this.readConfig('sentryProjectSlug')
+ '/releases/'
+ this.readConfig('revisionKey')
+ '/';
didDeploy(/* context */) {
let deployMessage =
'Uploaded sourcemaps to sentry release: ' +
this.readConfig('sentryUrl') +
'/' +
this.readConfig('sentryOrganizationSlug') +
'/' +
this.readConfig('sentryProjectSlug') +
'/releases/' +
this.readConfig('revisionKey') +
'/';

this.log(deployMessage);
}
Expand Down
Loading