install-artifact-from-github
Version:
Create binary artifacts hosted by github and install them without compiling.
177 lines (160 loc) • 6.33 kB
JavaScript
;
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);
});