From 52e9af0950c72581cc75838185aed630e66af1ee Mon Sep 17 00:00:00 2001 From: Allen Luce Date: Fri, 3 Aug 2018 15:32:51 -0700 Subject: [PATCH] Support Github Enterprise, upgrade Github client package @octokit/rest is the new (since January) NPM package for accessing the Github API. There are some differences that are taken care of here. One of them is setting the mime type on asset upload. Since octokit initialization happens on init (instead of module execution), I've hacked up a means to use Sinon for mocking it. The primary result of this change is to allow for publishing to Github Enterprise instances. --- index.js | 112 +++++++++++++++++++++------------------------------ package.json | 6 ++- test/test.js | 60 +++++++++++++++------------ 3 files changed, 85 insertions(+), 93 deletions(-) diff --git a/index.js b/index.js index 9c7879c..b1c0f94 100644 --- a/index.js +++ b/index.js @@ -2,7 +2,7 @@ var path = require("path"); var fs = require('fs'); -var GitHubApi = require("github"); +var mime = require("mime-types"); var cwd = process.cwd(); var verbose; @@ -11,25 +11,8 @@ var consoleLog = function(x){ }; function NodePreGypGithub() {} - -NodePreGypGithub.prototype.github = new GitHubApi({ // set defaults - // required - version: "3.0.0", - // optional - debug: false, - protocol: "https", - host: "api.github.com", - pathPrefix: "", // for some GHEs; none for GitHub - timeout: 5000, - headers: {} -}); - -NodePreGypGithub.prototype.owner = ""; -NodePreGypGithub.prototype.repo = ""; -NodePreGypGithub.prototype.package_json = {}; -NodePreGypGithub.prototype.release = {}; +NodePreGypGithub.prototype.octokit = require("@octokit/rest"); NodePreGypGithub.prototype.stage_dir = path.join(cwd,"build","stage"); - NodePreGypGithub.prototype.init = function() { var ownerRepo, hostPrefix; @@ -39,25 +22,30 @@ NodePreGypGithub.prototype.init = function() { throw new Error('Missing repository.url in package.json'); } else { - ownerRepo = this.package_json.repository.url.match(/github\.com\/(.*)(?=\.git)/i); + ownerRepo = this.package_json.repository.url.match(/https?:\/\/([^\/]+)\/(.*)(?=\.git)/i); if(ownerRepo) { - ownerRepo = ownerRepo[1].split('/'); + this.host = ownerRepo[1]; + ownerRepo = ownerRepo[2].split('/'); this.owner = ownerRepo[0]; this.repo = ownerRepo[1]; } else throw new Error('A correctly formatted GitHub repository.url was not found within package.json'); } - hostPrefix = 'https://github.com/' + this.owner + '/' + this.repo + '/releases/download/'; + hostPrefix = 'https://' + this.host + '/' + this.owner + '/' + this.repo + '/releases/download/'; if(!this.package_json.binary || 'object' !== typeof this.package_json.binary || 'string' !== typeof this.package_json.binary.host){ throw new Error('Missing binary.host in package.json'); } else if (this.package_json.binary.host.substr(0, hostPrefix.length) !== hostPrefix){ throw new Error('binary.host in package.json should begin with: "' + hostPrefix + '"'); } - - this.github.headers = {"user-agent": (this.package_json.name) ? this.package_json.name : "node-pre-gyp-github"}; // GitHub is happy with a unique user agent - + + this.octokit = NodePreGypGithub.prototype.octokit({ + baseUrl: 'https://' + this.host + '/api/v3', + headers: { + "user-agent": (this.package_json.name) ? this.package_json.name : "node-pre-gyp-github" + } + }); }; NodePreGypGithub.prototype.authenticate_settings = function(){ @@ -71,6 +59,7 @@ NodePreGypGithub.prototype.authenticate_settings = function(){ NodePreGypGithub.prototype.createRelease = function(args, callback) { var options = { + 'host': this.host, 'owner': this.owner, 'repo': this.repo, 'tag_name': this.package_json.version, @@ -86,19 +75,21 @@ NodePreGypGithub.prototype.createRelease = function(args, callback) { options[key] = args[key]; } }); - - this.github.authenticate(this.authenticate_settings()); - this.github.releases.createRelease(options, callback); + this.octokit.authenticate(this.authenticate_settings()); + this.octokit.repos.createRelease(options, callback); }; NodePreGypGithub.prototype.uploadAsset = function(cfg){ - this.github.authenticate(this.authenticate_settings()); - this.github.releases.uploadAsset({ + this.octokit.authenticate(this.authenticate_settings()); + this.octokit.repos.uploadAsset({ + url: this.release.upload_url, owner: this.owner, id: this.release.id, repo: this.repo, name: cfg.fileName, - filePath: cfg.filePath + file: cfg.filePath, + contentType: mime.contentType(cfg.fileName) || 'application/octet-stream', + contentLength: fs.statSync(cfg.filePath).size, }, function(err){ if(err) throw err; consoleLog('Staged file ' + cfg.fileName + ' saved to ' + this.owner + '/' + this.repo + ' release ' + this.release.tag_name + ' successfully.'); @@ -114,19 +105,19 @@ NodePreGypGithub.prototype.uploadAssets = function(){ if(!files.length) throw new Error('No files found within the stage directory: ' + this.stage_dir); files.forEach(function(file){ - asset = this.release.assets.filter(function(element, index, array){ - return element.name === file; + if(this.release && this.release.assets) { + asset = this.release.assets.filter(function(element, index, array){ + return element.name === file; + }); + if(asset.length) { + throw new Error("Staged file " + file + " found but it already exists in release " + this.release.tag_name + ". If you would like to replace it, you must first manually delete it within GitHub."); + } + } + consoleLog("Staged file " + file + " found. Proceeding to upload it."); + this.uploadAsset({ + fileName: file, + filePath: path.join(this.stage_dir, file) }); - if(asset.length) { - throw new Error("Staged file " + file + " found but it already exists in release " + this.release.tag_name + ". If you would like to replace it, you must first manually delete it within GitHub."); - } - else { - consoleLog("Staged file " + file + " found. Proceeding to upload it."); - this.uploadAsset({ - fileName: file, - filePath: path.join(this.stage_dir, file) - }); - } }.bind(this)); }.bind(this)); }; @@ -135,13 +126,12 @@ NodePreGypGithub.prototype.publish = function(options) { options = (typeof options === 'undefined') ? {} : options; verbose = (typeof options.verbose === 'undefined' || options.verbose) ? true : false; this.init(); - this.github.authenticate(this.authenticate_settings()); - this.github.releases.listReleases({ + this.octokit.authenticate(this.authenticate_settings()); + this.octokit.repos.getReleases({ 'owner': this.owner, 'repo': this.repo }, function(err, data){ var release; - if(err) throw err; // when remote_path is set expect files to be in stage_dir / remote_path after substitution @@ -152,37 +142,27 @@ NodePreGypGithub.prototype.publish = function(options) { // This is here for backwards compatibility for before binary.remote_path support was added in version 1.2.0. options.tag_name = this.package_json.version; } - - release = (function(){ // create a new array containing only those who have a matching version. - if(data) { - data = data.filter(function(element, index, array){ - return element.tag_name === options.tag_name; - }.bind(this)); - return data; - } - else return []; - }.bind(this))(); - - this.release = release[0]; - - if(!release.length) { + release = data.data.filter(function(element, index, array){ + return element.tag_name === options.tag_name; + }); + if(release.length === 0) { this.createRelease(options, function(err, release) { if(err) throw err; - - this.release = release; - if (release.draft) { - consoleLog('Release ' + release.tag_name + " not found, so a draft release was created. YOU MUST MANUALLY PUBLISH THIS DRAFT WITHIN GITHUB FOR IT TO BE ACCESSIBLE."); + this.release = release.data; + if (this.release.draft) { + consoleLog('Release ' + this.release.tag_name + " not found, so a draft release was created. YOU MUST MANUALLY PUBLISH THIS DRAFT WITHIN GITHUB FOR IT TO BE ACCESSIBLE."); } else { consoleLog('Release ' + release.tag_name + " not found, so a new release was created and published."); } - this.uploadAssets(); + this.uploadAssets(this.release.upload_url); }.bind(this)); } else { + this.release = release[0]; this.uploadAssets(); } }.bind(this)); }; -module.exports = NodePreGypGithub; \ No newline at end of file +module.exports = NodePreGypGithub; diff --git a/package.json b/package.json index a99be2a..0644e05 100644 --- a/package.json +++ b/package.json @@ -25,8 +25,10 @@ "releases" ], "dependencies": { - "github": "^0.2.4", - "commander": "^2.9.0" + "@octokit/rest": "^15.9.5", + "commander": "^2.9.0", + "mime-types": "^2.1.19", + "sinon": "^6.1.4" }, "devDependencies": { "chai": "^3.5.0", diff --git a/test/test.js b/test/test.js index 7071d7c..0564081 100644 --- a/test/test.js +++ b/test/test.js @@ -5,23 +5,31 @@ var fs = require("fs"); var Index = require('../index.js'); var index = new Index(); var stage_dir = index.stage_dir; +var Octokit = require("@octokit/rest"); +var octokit = Octokit(); +var sinon = require('sinon') var reset_index = function(index_string_ref) { delete require.cache[require.resolve(index_string_ref)]; return require(index_string_ref); }; + +var sandbox = sinon.createSandbox(); + var reset_mocks = function() { + sandbox.restore(); process.env.NODE_PRE_GYP_GITHUB_TOKEN = "secret"; fs = reset_index('fs'); fs.readFileSync = function(){return '{"name":"test","version":"0.0.1","repository": {"url":"git+https://github.com/test/test.git"},"binary":{"host":"https://github.com/test/test/releases/download/","remote_path":"{version}"}}';}; index.stage_dir = stage_dir; - index.github.authenticate = function(){}; - index.github.releases.listReleases = function(options, cb){ - cb(null, [{"tag_name":"0.0.0","assets":[{"name":"filename"}]}]); - }; - index.github.releases.createRelease = function(options, cb){ - cb(null,{"tag_name":"0.0.1","draft":true,"assets":[{}]}); - }; - index.github.releases.uploadAsset = function(cfg,cb){cb();}; + Index.prototype.octokit = function() {return octokit;}; + sandbox.stub(octokit, 'authenticate'); + sandbox.stub(octokit.repos, 'getReleases').callsFake(function(options, cb){ + cb(null, {data: [{"tag_name":"0.0.0","assets":[{"name":"filename"}]}]}); + }); + sandbox.stub(octokit.repos, 'createRelease').callsFake(function(options, cb){ + cb(null,{data: {"tag_name":"0.0.1","draft":true,"assets":[{}]}}); + }); + sandbox.stub(octokit.repos, 'uploadAsset').callsFake(function(cfg,cb){cb();}); }; if(!process.env.COVERALLS_SERVICE_NAME) console.log('To post to coveralls.io, be sure to set COVERALLS_SERVICE_NAME environment variable'); @@ -37,9 +45,8 @@ describe("Publishes packages to GitHub Releases", function() { fs.readdir = function(filename, cb) { cb(null,["filename"]); }; - index.github.releases.createRelease = function(options, cb){ - cb(null,{"tag_name":"0.0.1","draft":false,"assets":[{}]}); - }; + fs.statSync = function() {return 0;} + index.publish(options) expect(function(){ index.publish(options); }).to.not.throw(); }); @@ -91,22 +98,24 @@ describe("Publishes packages to GitHub Releases", function() { expect(function(){ index.publish(options); }).to.throw("NODE_PRE_GYP_GITHUB_TOKEN environment variable not found"); }); - it("should throw an error when github.releases.listReleases returns an error", function() { + it("should throw an error when octokit.repos.getReleases returns an error", function() { var options = {'draft': true, 'verbose': false}; reset_mocks(); - index.github.releases.listReleases = function(options, cb){ - cb(new Error('listReleases error')); - }; - expect(function(){ index.publish(options); }).to.throw('listReleases error'); + + octokit.repos.getReleases.restore(); + sandbox.stub(octokit.repos, 'getReleases').callsFake(function(options, cb){ + cb(new Error('getReleases error')); + }); + expect(function(){ index.publish(options); }).to.throw('getReleases error'); }); it("should throw an error when github.releases.createRelease returns an error", function() { var options = {'draft': true, 'verbose': false}; reset_mocks(); - index.github.releases.listReleases = function(options, cb){ - cb(null,null); + octokit.repos.getReleases = function(options, cb){ + cb(null,{data: []}); }; - index.github.releases.createRelease = function(options, cb){ + octokit.repos.createRelease = function(options, cb){ cb(new Error('createRelease error')); }; expect(function(){ index.publish(options); }).to.throw('createRelease error'); @@ -136,8 +145,8 @@ describe("Publishes packages to GitHub Releases", function() { fs.readdir = function(filename, cb) { cb(null,["filename"]); }; - index.github.releases.listReleases = function(options, cb){ - cb(null, [{"tag_name":"0.0.1","assets":[{"name":"filename"}]}]); + octokit.repos.getReleases = function(options, cb){ + cb(null, {data: [{"tag_name":"0.0.1","assets":[{"name":"filename"}]}]}); }; expect(function(){ index.publish(options); }).to.throw(/^Staged file .* found but it already exists in release .*. If you would like to replace it, you must first manually delete it within GitHub./i); }); @@ -148,7 +157,7 @@ describe("Publishes packages to GitHub Releases", function() { fs.readdir = function(filename, cb) { cb(null,["filename"]); }; - index.github.releases.uploadAsset = function(cfg,cb){ + octokit.repos.uploadAsset = function(cfg,cb){ cb(new Error('uploadAsset error')); }; expect(function(){ index.publish(options); }).to.throw("uploadAsset error"); @@ -165,11 +174,12 @@ describe("Publishes packages to GitHub Releases", function() { fs.readdir = function(filename, cb) { cb(null,["filename"]); }; - index.github.releases.createRelease = function(options, cb){ - cb(null,{"tag_name":"0.0.1","draft":false,"assets":[{}]}); + fs.statSync = function() {return 0;} + octokit.reposcreateRelease = function(options, cb){ + cb(null,{data: {"tag_name":"0.0.1","draft":false,"assets":[{}]}}); }; expect(function(){ index.publish(options); }).to.not.throw(); }); }); -}); \ No newline at end of file +});