Skip to content

Instantly share code, notes, and snippets.

@bdombro
Created March 10, 2022 22:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bdombro/c032cb979ed7369f9ded7085f85ba422 to your computer and use it in GitHub Desktop.
Save bdombro/c032cb979ed7369f9ded7085f85ba422 to your computer and use it in GitHub Desktop.
A shared lib for zx
#!/usr/bin/env zx
/**
* Util/Lib functions for zx scripts
*/
/* eslint-disable i18next/no-literal-string */
/* eslint-disable no-console */
export class HandledError extends Error {}
export function throwHandledError(message) {
throw new HandledError(message);
}
export function throwError(message) {
throw new Error(message);
}
if (!globalThis.$) {
throwError('This script must be ran using zx (https://github.com/google/zx)');
}
export async function assertGit() {
$`which git`.catch(_ =>
throwError('This script requires git to be installed'),
);
}
export async function assertGithubCli() {
await $`which gh`.catch(_ =>
throwError(
'This script requires github CLI (gh) to be installed and configured',
),
);
await $`gh auth status -h ghe.spotify.net`.catch(_ =>
throwError(
'This script requires github CLI (gh) to authenticated with ghe.spotify.net',
),
);
}
export async function silenced(fn) {
return async () => {
const verboseBefore = $.verbose;
$.verbose = false;
await promisified(fn)();
$.verbose = verboseBefore;
console.log('hello2');
};
}
export function log(msg, shouldReplaceLast) {
return shouldReplaceLast
? process.stdout.write(`${msg}\r`)
: console.log(msg);
}
log.info = async (msg, shouldReplaceLast) => {
const msgArray = Array.isArray(msg) ? msg : [msg];
for (const m of msgArray) {
await log(chalk.gray(' - ' + m), shouldReplaceLast);
}
};
log.announce = (msg, shouldReplaceLast) =>
log(chalk.black.bgGreen(msg), shouldReplaceLast);
log.warn = (msg, shouldReplaceLast) =>
log(chalk.white.bgOrange(msg), shouldReplaceLast);
log.error = (msg, shouldReplaceLast) =>
log(chalk.white.bgRed(msg), shouldReplaceLast);
log.newLine = () => log('');
log.say = async msg => !$.silent && $`say ${msg}`;
export async function waitWithTimer(
cmdPromise,
prefix = '',
logger = log.info,
) {
let running = true;
let seconds = 0;
const resPromise = cmdPromise.finally(() => (running = false));
while (running) {
await logger(`${prefix}${seconds++}s`, seconds);
await sleep(1000);
}
log.newLine();
return await resPromise;
}
export const getGitCtxT = throttle(getGitCtx, 10000);
export async function getGitCtx(includePrCtx) {
const verboseBefore = $.verbose;
$.verbose = false;
await $`git fetch`;
const branch = (await $`git branch --show-current`).stdout.replace('\n', '');
const isLocalOnly = await $`git fetch origin ${branch}`.then(() => false).catch(() => true);
const isBehind = Number(
await $`git rev-list --left-only --count origin/master...${branch}`,
);
const localStatus = (await $`git status --porcelain`).stdout
.split('\n')
.map(line => line.trim())
.filter(Boolean)
.reduce((acc, line) => {
acc.isDirty = true;
acc.isClean = false;
const [type, file] = line.split(' ');
const typeMap = { M: 'modified', A: 'added', D: 'deleted' };
acc[typeMap[type]].push(file);
return acc;
}, { modified: [], added: [], deleted: [], isDirty: false, isClean: true });
;
const ctx = {
branch,
isBehind,
isLocalOnly,
...localStatus,
};
if (includePrCtx) {
const prViewJson = JSON.parse(
await $`gh pr view --json number,state,mergeable,reviewDecision,statusCheckRollup,closed,mergedAt`,
);
// Sometimes status checks are not ready yet, so we should assume pending
if (!prViewJson.statusCheckRollup?.length) {
prViewJson.statusCheckRollup = [{ state: 'PENDING' }];
}
const prCtx = {
...prViewJson,
checks: {
failure: prViewJson.statusCheckRollup.filter(s => s.state === 'FAILURE')
.length,
successful: prViewJson.statusCheckRollup.filter(s => s.state === 'SUCCESS')
.length,
pending: prViewJson.statusCheckRollup.filter(s => s.state === 'PENDING')
.length,
},
};
Object.assign(ctx, prCtx);
}
$.verbose = verboseBefore;
if ($.verbose) console.log(ctx);
return ctx;
}
export function promisify(fn) {
return async () => fn();
}
/**
* Returns the last response from a function if called again before wait interval
*/
export function throttle(fn, wait = 1e3) {
let last = 0;
let lastRes = null;
return function throttled() {
const now = Date.now();
if (now - last < wait) return lastRes;
last = now;
lastRes = fn.apply(this, arguments);
return lastRes;
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment