2019-03-12 22:07:57 +00:00
|
|
|
/**
|
|
|
|
* @license
|
|
|
|
* Copyright 2019 Balena Ltd.
|
|
|
|
*
|
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
* you may not use this file except in compliance with the License.
|
|
|
|
* You may obtain a copy of the License at
|
|
|
|
*
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
*
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
* See the License for the specific language governing permissions and
|
|
|
|
* limitations under the License.
|
|
|
|
*/
|
2020-01-24 22:20:39 +00:00
|
|
|
|
2019-05-23 00:44:08 +00:00
|
|
|
import * as Bluebird from 'bluebird';
|
|
|
|
import * as _ from 'lodash';
|
2019-08-23 03:02:59 +00:00
|
|
|
import * as semver from 'semver';
|
2019-03-12 22:07:57 +00:00
|
|
|
|
2019-06-02 14:23:37 +00:00
|
|
|
import { finalReleaseAssets, version } from './build-bin';
|
2017-12-13 17:33:03 +00:00
|
|
|
|
2019-06-02 14:23:37 +00:00
|
|
|
const { GITHUB_TOKEN } = process.env;
|
2019-05-23 00:44:08 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Create or update a release in GitHub's releases page, uploading the
|
|
|
|
* installer files (standalone zip + native oclif installers).
|
|
|
|
*/
|
|
|
|
export async function createGitHubRelease() {
|
|
|
|
console.log(`Publishing release ${version} to GitHub`);
|
2019-08-23 03:02:59 +00:00
|
|
|
const publishRelease = await import('publish-release');
|
2019-05-23 00:44:08 +00:00
|
|
|
const ghRelease = await Bluebird.fromCallback(
|
|
|
|
publishRelease.bind(null, {
|
2019-03-12 22:07:57 +00:00
|
|
|
token: GITHUB_TOKEN || '',
|
2018-10-19 14:38:50 +00:00
|
|
|
owner: 'balena-io',
|
|
|
|
repo: 'balena-cli',
|
2018-01-09 15:05:24 +00:00
|
|
|
tag: version,
|
2018-10-19 14:38:50 +00:00
|
|
|
name: `balena-CLI ${version}`,
|
2018-01-09 15:05:24 +00:00
|
|
|
reuseRelease: true,
|
2019-05-23 00:44:08 +00:00
|
|
|
assets: finalReleaseAssets[process.platform],
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
console.log(`Release ${version} successful: ${ghRelease.html_url}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Top-level function to create a CLI release in GitHub's releases page:
|
|
|
|
* call zipStandaloneInstaller(), rename the files as we'd like them to
|
|
|
|
* display on the releases page, and call createGitHubRelease() to upload
|
|
|
|
* the files.
|
|
|
|
*/
|
|
|
|
export async function release() {
|
|
|
|
try {
|
|
|
|
await createGitHubRelease();
|
|
|
|
} catch (err) {
|
2018-01-09 15:05:24 +00:00
|
|
|
console.error('Release failed');
|
|
|
|
console.error(err);
|
|
|
|
process.exit(1);
|
2019-05-23 00:44:08 +00:00
|
|
|
}
|
|
|
|
}
|
2019-08-23 03:02:59 +00:00
|
|
|
|
|
|
|
/** Return a cached Octokit instance, creating a new one as needed. */
|
2020-06-15 22:53:07 +00:00
|
|
|
const getOctokit = _.once(function () {
|
2021-07-20 13:57:00 +00:00
|
|
|
const Octokit = (
|
|
|
|
require('@octokit/rest') as typeof import('@octokit/rest')
|
|
|
|
).Octokit.plugin(
|
|
|
|
(
|
|
|
|
require('@octokit/plugin-throttling') as typeof import('@octokit/plugin-throttling')
|
|
|
|
).throttling,
|
2019-08-23 03:02:59 +00:00
|
|
|
);
|
2020-01-24 22:20:39 +00:00
|
|
|
return new Octokit({
|
2019-08-23 03:02:59 +00:00
|
|
|
auth: GITHUB_TOKEN,
|
|
|
|
throttle: {
|
|
|
|
onRateLimit: (retryAfter: number, options: any) => {
|
|
|
|
console.warn(
|
2020-01-20 21:21:05 +00:00
|
|
|
`Request quota exhausted for request ${options.method} ${options.url}`,
|
2019-08-23 03:02:59 +00:00
|
|
|
);
|
|
|
|
// retries 3 times
|
|
|
|
if (options.request.retryCount < 3) {
|
|
|
|
console.log(`Retrying after ${retryAfter} seconds!`);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
onAbuseLimit: (_retryAfter: number, options: any) => {
|
|
|
|
// does not retry, only logs a warning
|
|
|
|
console.warn(
|
|
|
|
`Abuse detected for request ${options.method} ${options.url}`,
|
|
|
|
);
|
|
|
|
},
|
|
|
|
},
|
2020-01-24 22:20:39 +00:00
|
|
|
});
|
|
|
|
});
|
2019-08-23 03:02:59 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Extract pagination information (current page, total pages, ordinal number)
|
|
|
|
* from the 'link' response header (example below), using the parse-link-header
|
|
|
|
* npm package:
|
|
|
|
* "link": "<https://api.github.com/repositories/187370853/releases?per_page=2&page=2>; rel=\"next\",
|
|
|
|
* <https://api.github.com/repositories/187370853/releases?per_page=2&page=3>; rel=\"last\""
|
|
|
|
*
|
|
|
|
* @param response Octokit response object (including response.headers.link)
|
|
|
|
* @param perPageDefault Default per_page pagination value if missing in URL
|
|
|
|
* @return Object where 'page' is the current page number (1-based),
|
|
|
|
* 'pages' is the total number of pages, and 'ordinal' is the ordinal number
|
|
|
|
* (3rd, 4th, 5th...) of the first item in the current page.
|
|
|
|
*/
|
|
|
|
function getPageNumbers(
|
|
|
|
response: any,
|
|
|
|
perPageDefault: number,
|
|
|
|
): { page: number; pages: number; ordinal: number } {
|
|
|
|
const res = { page: 1, pages: 1, ordinal: 1 };
|
|
|
|
if (!response.headers.link) {
|
|
|
|
return res;
|
|
|
|
}
|
2021-07-20 13:57:00 +00:00
|
|
|
const parse =
|
|
|
|
require('parse-link-header') as typeof import('parse-link-header');
|
2019-08-23 03:02:59 +00:00
|
|
|
const parsed = parse(response.headers.link);
|
2020-06-30 19:54:35 +00:00
|
|
|
if (parsed == null) {
|
|
|
|
throw new Error(`Failed to parse link header: '${response.headers.link}'`);
|
|
|
|
}
|
2019-08-23 03:02:59 +00:00
|
|
|
let perPage = perPageDefault;
|
|
|
|
if (parsed.next) {
|
|
|
|
if (parsed.next.per_page) {
|
|
|
|
perPage = parseInt(parsed.next.per_page, 10);
|
|
|
|
}
|
|
|
|
res.page = parseInt(parsed.next.page, 10) - 1;
|
|
|
|
res.pages = parseInt(parsed.last.page, 10);
|
|
|
|
} else {
|
|
|
|
if (parsed.prev.per_page) {
|
|
|
|
perPage = parseInt(parsed.prev.per_page, 10);
|
|
|
|
}
|
|
|
|
res.page = res.pages = parseInt(parsed.prev.page, 10) + 1;
|
|
|
|
}
|
|
|
|
res.ordinal = (res.page - 1) * perPage + 1;
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Iterate over every GitHub release in the given owner/repo, check whether
|
|
|
|
* its tag_name matches against the affectedVersions semver spec, and if so
|
|
|
|
* replace its release description (body) with the given newDescription value.
|
|
|
|
* @param owner GitHub repo owner, e.g. 'balena-io' or 'pdcastro'
|
|
|
|
* @param repo GitHub repo, e.g. 'balena-cli'
|
|
|
|
* @param affectedVersions Semver spec, e.g. '2.6.1 - 7.10.9 || 8.0.0'
|
|
|
|
* @param newDescription New release description (body)
|
|
|
|
* @param editID Short string present in newDescription, e.g. '[AA101]', that
|
|
|
|
* can be searched to determine whether that release has already been updated.
|
|
|
|
*/
|
|
|
|
async function updateGitHubReleaseDescriptions(
|
|
|
|
owner: string,
|
|
|
|
repo: string,
|
|
|
|
affectedVersions: string,
|
|
|
|
newDescription: string,
|
|
|
|
editID: string,
|
|
|
|
) {
|
|
|
|
const perPage = 30;
|
|
|
|
const octokit = getOctokit();
|
|
|
|
const options = await octokit.repos.listReleases.endpoint.merge({
|
|
|
|
owner,
|
|
|
|
repo,
|
|
|
|
per_page: perPage,
|
|
|
|
});
|
|
|
|
let errCount = 0;
|
2021-07-20 13:57:00 +00:00
|
|
|
type Release =
|
|
|
|
import('@octokit/rest').RestEndpointMethodTypes['repos']['listReleases']['response']['data'][0];
|
2021-07-09 13:44:38 +00:00
|
|
|
for await (const response of octokit.paginate.iterator<Release>(options)) {
|
2021-07-20 13:57:00 +00:00
|
|
|
const {
|
|
|
|
page: thisPage,
|
|
|
|
pages: totalPages,
|
|
|
|
ordinal,
|
|
|
|
} = getPageNumbers(response, perPage);
|
2019-08-23 03:02:59 +00:00
|
|
|
let i = 0;
|
|
|
|
for (const cliRelease of response.data) {
|
|
|
|
const prefix = `[#${ordinal + i++} pg ${thisPage}/${totalPages}]`;
|
|
|
|
if (!cliRelease.id) {
|
|
|
|
console.error(
|
|
|
|
`${prefix} Error: missing release ID (errCount=${++errCount})`,
|
|
|
|
);
|
|
|
|
continue;
|
|
|
|
}
|
2020-01-20 21:21:05 +00:00
|
|
|
const skipMsg = `${prefix} skipping release "${cliRelease.tag_name}" (${cliRelease.id})`;
|
2019-08-23 03:02:59 +00:00
|
|
|
if (cliRelease.draft === true) {
|
|
|
|
console.info(`${skipMsg}: draft release`);
|
|
|
|
continue;
|
|
|
|
} else if (cliRelease.body && cliRelease.body.includes(editID)) {
|
|
|
|
console.info(`${skipMsg}: already updated`);
|
|
|
|
continue;
|
|
|
|
} else if (!semver.satisfies(cliRelease.tag_name, affectedVersions)) {
|
|
|
|
console.info(`${skipMsg}: outside version range`);
|
|
|
|
continue;
|
|
|
|
} else {
|
|
|
|
const updatedRelease = {
|
|
|
|
owner,
|
|
|
|
repo,
|
|
|
|
release_id: cliRelease.id,
|
|
|
|
body: newDescription,
|
|
|
|
};
|
|
|
|
let oldBodyPreview = cliRelease.body;
|
|
|
|
if (oldBodyPreview) {
|
|
|
|
oldBodyPreview = oldBodyPreview.replace(/\s+/g, ' ').trim();
|
|
|
|
if (oldBodyPreview.length > 12) {
|
|
|
|
oldBodyPreview = oldBodyPreview.substring(0, 9) + '...';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
console.info(
|
2020-01-20 21:21:05 +00:00
|
|
|
`${prefix} updating release "${cliRelease.tag_name}" (${cliRelease.id}) old body="${oldBodyPreview}"`,
|
2019-08-23 03:02:59 +00:00
|
|
|
);
|
|
|
|
try {
|
|
|
|
await octokit.repos.updateRelease(updatedRelease);
|
|
|
|
} catch (err) {
|
|
|
|
console.error(
|
|
|
|
`${skipMsg}: Error: ${err.message} (count=${++errCount})`,
|
|
|
|
);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Add a warning description to CLI releases affected by a mixpanel tracking
|
|
|
|
* security issue (#1359). This function can be executed "manually" with the
|
|
|
|
* following command line:
|
|
|
|
*
|
|
|
|
* npx ts-node --type-check -P automation/tsconfig.json automation/run.ts fix1359
|
|
|
|
*/
|
|
|
|
export async function updateDescriptionOfReleasesAffectedByIssue1359() {
|
|
|
|
// Run only on Linux/Node10, instead of all platform/Node combinations.
|
|
|
|
// (It could have been any other platform, as long as it only runs once.)
|
|
|
|
if (process.platform !== 'linux' || semver.major(process.version) !== 10) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const owner = 'balena-io';
|
|
|
|
const repo = 'balena-cli';
|
|
|
|
const affectedVersions =
|
|
|
|
'2.6.1 - 7.10.9 || 8.0.0 - 8.1.0 || 9.0.0 - 9.15.6 || 10.0.0 - 10.17.5 || 11.0.0 - 11.7.2';
|
|
|
|
const editID = '[AA100]';
|
|
|
|
let newDescription = `
|
|
|
|
Please note: the "login" command in this release is affected by a
|
|
|
|
security issue fixed in versions
|
|
|
|
[7.10.10](https://github.com/balena-io/balena-cli/releases/tag/v7.10.10),
|
|
|
|
[8.1.1](https://github.com/balena-io/balena-cli/releases/tag/v8.1.1),
|
|
|
|
[9.15.7](https://github.com/balena-io/balena-cli/releases/tag/v9.15.7),
|
|
|
|
[10.17.6](https://github.com/balena-io/balena-cli/releases/tag/v10.17.6),
|
|
|
|
[11.7.3](https://github.com/balena-io/balena-cli/releases/tag/v11.7.3)
|
|
|
|
and later. If you need to use this version, avoid passing your password,
|
|
|
|
keys or tokens as command-line arguments. ${editID}`;
|
|
|
|
// remove line breaks and collapse white space
|
|
|
|
newDescription = newDescription.replace(/\s+/g, ' ').trim();
|
|
|
|
await updateGitHubReleaseDescriptions(
|
|
|
|
owner,
|
|
|
|
repo,
|
|
|
|
affectedVersions,
|
|
|
|
newDescription,
|
|
|
|
editID,
|
|
|
|
);
|
|
|
|
}
|