@process-engine/ci_tools
Version:
CI tools for process-engine.io
268 lines (215 loc) • 7.21 kB
text/typescript
import * as glob from 'glob';
import * as mime from 'mime-types';
import { Octokit } from '@octokit/rest';
import * as Path from 'path';
import * as yargsParser from 'yargs-parser';
import { readFileSync, statSync } from 'fs';
import { getCurrentRepoNameWithOwner, getFullCommitMessageFromRef, isExistingTag } from '../git/git';
import { getPackageVersionTag } from '../versions/package_version';
import { setupGit } from './internal/setup-git-and-npm-connections';
type GitTag = string;
type GitHubRepo = {
owner: string;
name: string;
};
const COMMAND_NAME = 'update-github-release';
const BADGE = `[${COMMAND_NAME}]\t`;
const DEFAULT_MODE = 'node';
const SKIP_CI_MESSAGE = '[skip ci]';
const DOC = `
Updates or creates a GitHub release using the current version (or given \`--version-tag\`).
Uploads all given \`--assets\`, resolving globs and updating existing assets on GitHub.
OPTIONS
--mode sets the package mode [dotnet, node, python] (default: node)
`;
// DOC: see above
export async function run(...args): Promise<boolean> {
const argv = yargsParser(args, { default: { mode: DEFAULT_MODE } });
const isDryRun = argv.dry;
const mode = argv.mode;
let versionTag = argv.versionTag;
let title = argv.title;
let text = argv.text;
setupGit();
if (versionTag == null) {
versionTag = await getPackageVersionTag(mode);
console.log(`${BADGE}No --version-tag given, versionTag set to:`, versionTag);
}
if (argv.useTitleAndTextFromGitTag) {
if (!isExistingTag(versionTag)) {
console.error(`${BADGE}Tag does not exists: ${versionTag}`);
console.error(`${BADGE}Aborting.`);
return false;
}
const { subject, body } = getFullCommitMessageFromRef(versionTag);
title = subject;
text = body.replace(SKIP_CI_MESSAGE, '').trim();
console.log(`${BADGE}`);
console.log(`${BADGE}Option --use-title-and-text-from-git-tag was given.`);
console.log(`${BADGE}title set to:`, title);
console.log(`${BADGE}text set to:`, text);
}
let assets = [];
if (argv.assets) {
const list = Array.isArray(argv.assets) ? argv.assets : [argv.assets];
list.forEach((pattern: string): void => {
const files = glob.sync(pattern);
assets = assets.concat(files);
});
console.log(`${BADGE}`);
console.log(`${BADGE}Option --assets was given:`, assets);
}
const success = await updateGitHubRelease(versionTag, title, text, assets, isDryRun);
if (success) {
console.log(`${BADGE}Success.`);
}
return success;
}
export function getShortDoc(): string {
return DOC.trim().split('\n')[0];
}
export function printHelp(): void {
console.log(
`Usage: ci_tools ${COMMAND_NAME} [--use-title-and-text-from-git-tag | --title [--text]] [--assets <asset-name-or-glob> ...]
[--version-tag] [--dry] [--mode <MODE>]`
);
console.log('');
console.log(DOC.trim());
}
/**
* Updates the corresponding GitHub release for `versionTag` using the given `title` and `text`.
*/
export async function updateGitHubRelease(
versionTag: GitTag,
title: string,
text: string,
assets: string[],
dryRun: boolean = false
): Promise<boolean> {
const repo = getCurrentRepoNameWithOwnerAsObject(getCurrentRepoNameWithOwner());
const octokit = createOctokit(process.env.GH_TOKEN);
const releaseId = await getExistingReleaseId(octokit, repo, versionTag);
const releaseExists = releaseId != null;
if (releaseExists) {
if (dryRun) {
console.log(`${BADGE}Would now update existing release. Skipping since this is a dry run!`);
return true;
}
console.log(`${BADGE}Updating existing release for ${versionTag} ...`);
return updateExistingRelease(octokit, repo, releaseId, title, text, assets);
}
if (dryRun) {
console.log(`${BADGE}Would now create a new release. Skipping since this is a dry run!`);
return true;
}
console.log(`${BADGE}Creating new release for ${versionTag} ...`);
return createNewRelease(octokit, repo, versionTag, title, text, assets);
}
async function updateExistingRelease(
octokit: Octokit,
repo: GitHubRepo,
releaseId: number,
title: string,
text: string,
assets: string[]
): Promise<boolean> {
const response = await octokit.repos.updateRelease({
owner: repo.owner,
repo: repo.name,
release_id: releaseId,
name: title,
body: text,
});
let success = response.status === 200;
if (success) {
const uploadUrl = response.data.upload_url;
for (const filename of assets) {
console.log(`${BADGE}- Uploading '${filename}' ...`);
try {
const uploadSuccess = await uploadAsset(octokit, uploadUrl, filename);
success = success && uploadSuccess;
} catch (error) {
const data = tryToParseJson(error.message);
const errorIsFromOctokitAndAssetAlreadyExists =
data?.errors?.length === 1 && data?.errors[0]?.code === 'already_exists';
if (errorIsFromOctokitAndAssetAlreadyExists) {
console.log(`${BADGE} INFO: Asset '${filename}' already exists.`);
} else {
throw error;
}
}
}
}
return success;
}
async function uploadAsset(octokit: Octokit, uploadUrl: string, filename: string): Promise<boolean> {
const buffer: Buffer = readFileSync(filename);
const name: string = Path.basename(filename).replace(' ', '_');
const contentLength: number = statSync(filename).size;
const contentType: string = mime.lookup(filename) || 'text/plain';
const options: any = {
url: uploadUrl,
file: buffer,
contentType: contentType,
contentLength: contentLength,
name: name,
};
const uploadResponse = await octokit.repos.uploadReleaseAsset(options);
return uploadResponse.status === 201;
}
async function createNewRelease(
octokit: Octokit,
repo: GitHubRepo,
versionTag: GitTag,
title: string,
text: string,
assets: string[]
): Promise<boolean> {
const isPrerelease = versionTag.match(/-/) != null;
const response = await octokit.repos.createRelease({
owner: repo.owner,
repo: repo.name,
tag_name: versionTag,
name: title,
body: text,
prerelease: isPrerelease,
});
const success = response.status === 201;
if (success) {
const releaseId = response.data.id;
return updateExistingRelease(octokit, repo, releaseId, title, text, assets);
}
return success;
}
function createOctokit(githubAuthToken: string): Octokit {
const octokit = new Octokit({
auth: githubAuthToken,
});
return octokit;
}
function getCurrentRepoNameWithOwnerAsObject(nameWithOwner: string): GitHubRepo {
const parts = nameWithOwner.split('/');
return {
name: parts[1],
owner: parts[0],
};
}
async function getExistingReleaseId(octokit: Octokit, repo: GitHubRepo, versionTag: GitTag): Promise<number | null> {
try {
const response = await octokit.repos.getReleaseByTag({
owner: repo.owner,
repo: repo.name,
tag: versionTag,
});
return response.data.id;
} catch (error) {
return null;
}
}
function tryToParseJson(text: string): any | null {
try {
return JSON.parse(text);
} catch (e) {
return null;
}
}