UNPKG

install-artifact-from-github

Version:

Create binary artifacts hosted by github and install them without compiling.

177 lines (160 loc) 6.33 kB
#!/usr/bin/env node 'use strict'; const {promises: fsp} = require('fs'); const path = require('path'); const zlib = require('zlib'); const {promisify} = require('util'); const https = require('https'); const {spawnSync} = require('child_process'); const spawnOptions = {encoding: 'utf8', env: process.env}; const getPlatform = () => { const platform = process.platform; if (platform !== 'linux') return platform; // detecting musl using algorithm from https://github.com/lovell/detect-libc under Apache License 2.0 let result = spawnSync('getconf', ['GNU_LIBC_VERSION'], spawnOptions); if (!result.status && !result.signal) return platform; result = spawnSync('ldd', ['--version'], spawnOptions); if (result.signal) return platform; if ((!result.status && result.stdout.toString().indexOf('musl') >= 0) || (result.status === 1 && result.stderr.toString().indexOf('musl') >= 0)) return platform + '-musl'; return platform; }; const platform = getPlatform(); const getParam = (name, defaultValue = '') => { const index = process.argv.indexOf('--' + name); if (index > 0) return process.argv[index + 1] || ''; return defaultValue; }; const cleanOptions = options => { const result = {}; for (const [key, value] of Object.entries(options)) { if (value === undefined || value === null) continue; if (key === 'headers') { result.headers = cleanOptions(value); continue; } result[key] = value; } return result; }; const io = (url, options = {}, data) => new Promise((resolve, reject) => { let buffer = null; options = cleanOptions(options); const req = https .request(url, options, res => { if (res.statusCode >= 300 && res.statusCode < 400 && res.headers && res.headers.location) { io(res.headers.location, options, data).then(resolve, reject); return; } if (res.statusCode < 200 || res.statusCode >= 300) { reject(Error(`Status ${res.statusCode} for ${url}`)); return; } res.on('data', data => { if (buffer) { buffer = Buffer.concat([buffer, data]); } else { buffer = data; } }); res.on('end', () => resolve({data: buffer, res})); }) .on('error', error => reject(error)); data && req.write(data); req.end(); }); const get = (url, options) => io(url, {agent: false, ...options, method: 'GET'}); const post = (url, options, data) => io(url, {agent: false, ...options, method: 'POST'}, data); const url = (parts, ...args) => { let result = parts[0] || ''; for (let i = 0; i < args.length; ) { result += encodeURIComponent(args[i]) + parts[++i]; } return new URL(result); }; const withParams = (url, params) => { const result = new URL(url); for (const [key, value] of Object.entries(params)) { result.searchParams.append(key, value); } return result; }; const artifactPath = getParam('artifact'), prefix = getParam('prefix'), suffix = getParam('suffix'); const main = async () => { const [OWNER, REPO] = process.env.GITHUB_REPOSITORY.split('/'), TAG = /^refs\/tags\/(.*)$/.exec(process.env.GITHUB_REF)[1], TOKEN = process.env.GITHUB_TOKEN, PERSONAL_TOKEN = process.env.PERSONAL_TOKEN; const fileName = `${prefix}${platform}-${process.arch}-${process.versions.modules}${suffix}`; console.log('Preparing artifact', fileName, '...'); const [data, uploadUrl] = await Promise.all([ fsp.readFile(path.normalize(artifactPath)), get(url`https://api.github.com/repos/${OWNER}/${REPO}/releases/tags/${TAG}`, { auth: TOKEN ? OWNER + ':' + TOKEN : null, headers: { Accept: 'application/vnd.github.v3+json', 'User-Agent': 'uhop/install-artifact-from-github', Authorization: !TOKEN && PERSONAL_TOKEN ? 'Bearer ' + PERSONAL_TOKEN : null } }).then(response => { const data = JSON.parse(response.data.toString()), p = data.upload_url.indexOf('{'); return p > 0 ? data.upload_url.substr(0, p) : data.upload_url; }) ]); console.log('Compressing and uploading ...'); await Promise.all([ (async () => { if (!zlib.brotliCompress) return null; const compressed = await promisify(zlib.brotliCompress)(data, {params: {[zlib.constants.BROTLI_PARAM_QUALITY]: zlib.constants.BROTLI_MAX_QUALITY}}), name = fileName + '.br', label = `Binary artifact: ${artifactPath} (${platform}, ${process.arch}, ${process.versions.modules}, brotli).`; return post( withParams(uploadUrl, {name, label}), { auth: TOKEN ? OWNER + ':' + TOKEN : null, headers: { Accept: 'application/vnd.github.v3+json', 'Content-Type': 'application/brotli', 'Content-Length': compressed.length, 'User-Agent': 'uhop/install-artifact-from-github', Authorization: !TOKEN && PERSONAL_TOKEN ? 'Bearer ' + PERSONAL_TOKEN : null } }, compressed ) .then(({res}) => console.log('Uploaded BR:', res.statusCode)) .catch(error => console.error('BR has failed to upload:', error)); })(), (async () => { if (!zlib.gzip) return null; const compressed = await promisify(zlib.gzip)(data, {level: zlib.constants.Z_BEST_COMPRESSION}), name = fileName + '.gz', label = `Binary artifact: ${artifactPath} (${platform}, ${process.arch}, ${process.versions.modules}, gzip).`; return post( withParams(uploadUrl, {name, label}), { auth: TOKEN ? OWNER + ':' + TOKEN : null, headers: { Accept: 'application/vnd.github.v3+json', 'Content-Type': 'application/gzip', 'Content-Length': compressed.length, 'User-Agent': 'uhop/install-artifact-from-github', Authorization: !TOKEN && PERSONAL_TOKEN ? 'Bearer ' + PERSONAL_TOKEN : null } }, compressed ) .then(({res}) => console.log('Uploaded GZ:', res.statusCode)) .catch(error => console.error('GZ has failed to upload:', error)); })() ]); console.log('Done.'); }; main().catch(error => { console.log('::error::' + ((error && error.message) || 'save-to-github-cache has failed')); process.exit(1); });