UNPKG

@mmomtchev/node-pre-gyp-github

Version:

A node-pre-gyp module which provides the ability to publish to GitHub releases.

241 lines (217 loc) 8.13 kB
'use strict'; const path = require('path'); const fs = require('fs'); const cwd = process.cwd(); let verbose; const consoleLog = function (x) { return verbose ? console.log(x) : false; }; function NodePreGypGithub() {} NodePreGypGithub.prototype.octokit = require('@octokit/rest').Octokit.plugin( require('@octokit/plugin-retry').retry ); NodePreGypGithub.prototype.stage_base_dir = path.join(cwd, 'build', 'stage'); NodePreGypGithub.prototype.init = function () { let ownerRepo, hostPrefix; this.token = process.env.NODE_PRE_GYP_GITHUB_TOKEN; if (!this.token) throw new Error('NODE_PRE_GYP_GITHUB_TOKEN environment variable not found'); this.package_json = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'))); if (!this.package_json.repository || !this.package_json.repository.url) { throw new Error('Missing repository.url in package.json'); } else { ownerRepo = this.package_json.repository.url.match(/https?:\/\/([^/]+)\/(.*)(?=\.git)/i); if (ownerRepo) { this.host = 'api.' + 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://' + 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 .replace('https://', 'https://api.') .substr(0, hostPrefix.length) !== hostPrefix ) { throw new Error('binary.host in package.json should begin with: "' + hostPrefix + '"'); } this.connect(); }; NodePreGypGithub.prototype.connect = function () { this.octokit = new NodePreGypGithub.prototype.octokit({ auth: this.token, baseUrl: 'https://' + this.host, headers: { 'user-agent': this.package_json.name ? this.package_json.name : 'node-pre-gyp-github' } }); }; NodePreGypGithub.prototype.createRelease = async function (args) { const options = { host: this.host, owner: this.owner, repo: this.repo, tag_name: this.package_json.version, name: 'v' + this.package_json.version, body: this.package_json.name + ' ' + this.package_json.version, draft: true, prerelease: false }; for (const key of Object.keys(args)) { if ( Object.prototype.hasOwnProperty.call(args, key) && Object.prototype.hasOwnProperty.call(options, key) ) { options[key] = args[key]; } } return this.octokit.repos.createRelease(options); }; NodePreGypGithub.prototype.deletePartialAssets = async function () { try { const assets = await this.octokit.repos.listReleaseAssets({ origin: this.release.upload_url, owner: this.owner, release_id: this.release.id, repo: this.repo }); const partial = assets.data.filter((a) => a.state === 'starter'); for (const asset of partial) await this.octokit.repos.deleteReleaseAsset({ origin: this.release.upload_url, owner: this.owner, release_id: this.release.id, repo: this.repo, asset_id: asset.id }); } catch (e) { console.error('Failed deleting partial assets for ', e); } }; NodePreGypGithub.prototype.uploadAsset = async function (cfg) { const data = await fs.promises.readFile(cfg.filePath); // Network errors leave an asset having a state of `starter` // that will block further upload attempts let lastErr; let retries = 3; do { try { await this.octokit.repos.uploadReleaseAsset({ origin: this.release.upload_url, owner: this.owner, release_id: this.release.id, repo: this.repo, name: cfg.fileName, data }); break; } catch (e) { retries--; console.error('Failed uploading ', e); console.error(`${retries} left`); console.error(''); lastErr = e; await this.deletePartialAssets(); } } while (retries > 0); if (retries == 0) throw lastErr; consoleLog( 'Staged file ' + cfg.fileName + ' saved to ' + this.owner + '/' + this.repo + ' release ' + this.release.tag_name + ' successfully.' ); }; NodePreGypGithub.prototype.uploadAssets = async function () { let asset; consoleLog('Stage directory path: ' + path.join(this.stage_dir)); const files = await fs.promises.readdir(path.join(this.stage_dir)); if (!files.length) throw new Error('No files found within the stage directory: ' + this.stage_dir); const q = []; for (const file of files) { if (this.release && this.release.assets) { asset = this.release.assets.filter(function (element) { 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.'); q.push( this.uploadAsset({ fileName: file, filePath: path.join(this.stage_dir, file) }) ); } return Promise.all(q); }; NodePreGypGithub.prototype.publish = async function (options) { options = typeof options === 'undefined' ? {} : options; verbose = typeof options.verbose === 'undefined' || options.verbose ? true : false; this.init(); const data = await this.octokit.repos.listReleases({ owner: this.owner, repo: this.repo }); // when remote_path is set expect files to be in stage_dir / remote_path after substitution if (this.package_json.binary.remote_path) { options.tag_name = this.package_json.binary.remote_path.replace( /\{version\}/g, this.package_json.version ); this.stage_dir = path.join(this.stage_base_dir, options.tag_name); } else { // 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; this.stage_dir = this.stage_base_dir; } const release = data.data.filter(function (element) { return element.tag_name === options.tag_name; }); if (release.length === 0) { const releaseNew = await this.createRelease(options); this.release = releaseNew.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.' ); } return this.uploadAssets(this.release.upload_url); } else { this.release = release[0]; return this.uploadAssets(); } }; module.exports = NodePreGypGithub;