From fbc53a1c888f265151147bfca06695b0f774a10d Mon Sep 17 00:00:00 2001 From: David Nalchevanidze Date: Mon, 10 Jun 2024 01:02:13 +0200 Subject: [PATCH] Scripts (#866) * scripts * config * config * releasy * package * update --- package-lock.json | 25 ++++++++++ package.json | 1 + scripts/cli.ts | 51 ++++++++++++++++++--- scripts/{lib => }/format.ts | 0 scripts/lib/changelog/fetch.ts | 81 --------------------------------- scripts/lib/changelog/index.ts | 22 --------- scripts/lib/changelog/render.ts | 71 ----------------------------- scripts/lib/changelog/types.ts | 44 ------------------ scripts/lib/gh.ts | 74 ------------------------------ scripts/lib/git.ts | 10 ---- scripts/lib/types.ts | 1 - scripts/{lib => }/utils.ts | 24 ---------- 12 files changed, 70 insertions(+), 334 deletions(-) rename scripts/{lib => }/format.ts (100%) delete mode 100644 scripts/lib/changelog/fetch.ts delete mode 100644 scripts/lib/changelog/index.ts delete mode 100644 scripts/lib/changelog/render.ts delete mode 100644 scripts/lib/changelog/types.ts delete mode 100644 scripts/lib/gh.ts delete mode 100644 scripts/lib/git.ts delete mode 100644 scripts/lib/types.ts rename scripts/{lib => }/utils.ts (64%) diff --git a/package-lock.json b/package-lock.json index 4d04b9eb9..757fb9a11 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@actions/core": "^1.9.1", "axios": "^0.26.1", "commander": "^9.1.0", + "gh-rel-easy": "^0.3.0", "glob": "^8.0.3", "js-yaml": "^4.1.0", "ramda": "^0.28.0", @@ -1211,6 +1212,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gh-rel-easy": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/gh-rel-easy/-/gh-rel-easy-0.3.0.tgz", + "integrity": "sha512-a7glQHdaWUudrMvX0MBAdQ4Z9QZKf84nxA1of29sPT9Rzgja6ry8s2e74Kc48qv8g0tNA10wGnApHi+gcghdhg==", + "dependencies": { + "axios": "^0.26.1", + "js-yaml": "^4.1.0", + "ramda": "^0.28.0", + "ts-node": "^10.7.0", + "typescript": "^4.6.2" + } + }, "node_modules/glob": { "version": "8.0.3", "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz", @@ -3030,6 +3043,18 @@ "integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==", "dev": true }, + "gh-rel-easy": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/gh-rel-easy/-/gh-rel-easy-0.3.0.tgz", + "integrity": "sha512-a7glQHdaWUudrMvX0MBAdQ4Z9QZKf84nxA1of29sPT9Rzgja6ry8s2e74Kc48qv8g0tNA10wGnApHi+gcghdhg==", + "requires": { + "axios": "^0.26.1", + "js-yaml": "^4.1.0", + "ramda": "^0.28.0", + "ts-node": "^10.7.0", + "typescript": "^4.6.2" + } + }, "glob": { "version": "8.0.3", "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz", diff --git a/package.json b/package.json index afa036491..0f3f5af10 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@actions/core": "^1.9.1", "axios": "^0.26.1", "commander": "^9.1.0", + "gh-rel-easy": "^0.3.0", "glob": "^8.0.3", "js-yaml": "^4.1.0", "ramda": "^0.28.0", diff --git a/scripts/cli.ts b/scripts/cli.ts index 45440e11e..78d529037 100644 --- a/scripts/cli.ts +++ b/scripts/cli.ts @@ -1,14 +1,44 @@ -import { github } from "./lib/gh"; -import { exit, write } from "./lib/utils"; -import { changelog } from "./lib/changelog"; +import { exit, hconf, write } from "./utils"; import { Command } from "commander"; -import { format } from "./lib/format"; +import { format } from "./format"; +import { GHRelEasy } from "gh-rel-easy"; const cli = new Command(); cli.name("cli").description("cli").version("0.0.0"); +const scope: Record = { + server: "morpheus-graphql", + client: "morpheus-graphql-client", + core: "morpheus-graphql-core", + subscriptions: "morpheus-graphql-subscriptions", + tests: "morpheus-graphql-tests", + app: "morpheus-graphql-app", +}; + +const relEasy = new GHRelEasy({ + pkg: (name) => `https://hackage.haskell.org/package/${scope[name]}`, + gh: { + org: "morpheusgraphql", + repo: "morpheus-graphql", + }, + scope, + pr: { + major: "Major Change", + breaking: "Breaking Change", + feature: "New features", + fix: "Bug Fixes", + chore: "Minor Changes", + }, + version: () => hconf("version"), + next: async (isBreaking) => { + await hconf("next", ...(isBreaking ? ["-b"] : [])); + + return hconf("version"); + }, +}); + cli .command("format") .description("format") @@ -22,13 +52,20 @@ release .command("open") .option("-p, --preview", "preview", false) .action(({ preview }: { preview: string }) => - changelog(true) - .then(preview ? () => Promise.resolve() : github.release) + relEasy + .changelog() + .then(async (body) => { + await hconf("setup"); + + if (preview) return; + + await relEasy.open(body); + }) .catch(exit) ); release .command("changelog") - .action(() => changelog().then(write("changelog.md")).catch(exit)); + .action(() => relEasy.changelog().then(write("changelog.md")).catch(exit)); cli.parse(); diff --git a/scripts/lib/format.ts b/scripts/format.ts similarity index 100% rename from scripts/lib/format.ts rename to scripts/format.ts diff --git a/scripts/lib/changelog/fetch.ts b/scripts/lib/changelog/fetch.ts deleted file mode 100644 index 195c0caca..000000000 --- a/scripts/lib/changelog/fetch.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { isNil, map, pluck, propEq, reject, uniq } from "ramda"; -import { github } from "../gh"; -import { Maybe } from "../types"; -import { getPRNumber } from "../utils"; -import { parseLabel, PR_TYPE, SCOPE } from "./types"; -import { commitsAfter } from "../git"; - -type Commit = { - message: string; - associatedPullRequests: { - nodes: Array<{ number: number; repository: { nameWithOwner: string } }>; - }; -}; - -const fetchCommits = github.batch( - (i) => - `object(oid: "${i}") { - ... on Commit { - message - associatedPullRequests(first: 10) { - nodes { - number - repository { nameWithOwner } - } - } - } - }` -); - -const toPRNumber = (c: Commit): Maybe => - c.associatedPullRequests.nodes.find(({ repository }) => - github.isOwner(repository) - )?.number ?? getPRNumber(c.message); - -type PR = { - number: number; - title: string; - body: string; - author: { login: string; url: string }; - labels: { nodes: { name: string }[] }; -}; - -const fetchPPs = github.batch( - (i) => - `pullRequest(number: ${i}) { - number - title - body - author { login url } - labels(first: 10) { nodes { name } } - }` -); - -type Change = PR & { - type: PR_TYPE; - scopes: SCOPE[]; -}; - -const toPRNumbers = (commit: Commit[]) => - uniq(reject(isNil, commit.map(toPRNumber))); - -const fetchChanges = (version: string) => - fetchCommits(commitsAfter(version)) - .then(toPRNumbers) - .then(fetchPPs) - .then( - map((pr): Change => { - const labels = pluck("name", pr.labels.nodes); - - return { - ...pr, - type: labels.map(parseLabel("pr")).find(Boolean) ?? "chore", - scopes: labels.map(parseLabel("scope")).filter(Boolean) as SCOPE[], - }; - }) - ); - -const isBreaking = (changes: Change[]) => - Boolean(changes.find(propEq("type", "breaking"))); - -export { fetchChanges, isBreaking, Change }; diff --git a/scripts/lib/changelog/index.ts b/scripts/lib/changelog/index.ts deleted file mode 100644 index 2dd6f7459..000000000 --- a/scripts/lib/changelog/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { fetchChanges, isBreaking } from "./fetch"; -import { render } from "./render"; -import { lastTag } from "../git"; -import { hconf } from "../utils"; - -export const changelog = async (change: boolean = false) => { - const version = lastTag(); - const projectVersion = await hconf("version"); - const changes = await fetchChanges(version); - - if (version !== projectVersion) { - throw Error(`versions does not match: ${version} ${projectVersion}`); - } - - await hconf("next", ...(isBreaking(changes) ? ["-b"] : [])); - - if (change) { - await hconf("setup"); - } - - return render(await hconf("version"), changes); -}; diff --git a/scripts/lib/changelog/render.ts b/scripts/lib/changelog/render.ts deleted file mode 100644 index 9e7f4e639..000000000 --- a/scripts/lib/changelog/render.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { groupBy, range } from "ramda"; -import { isKey } from "../utils"; -import { Change } from "./fetch"; -import { pullRequestTypes, config, SCOPE } from "./types"; -import { getDate } from "../git"; -import { github } from "../gh"; - -const link = (name: string, url: string) => `[${name}](${url})`; - -const packageURL = (name: SCOPE) => - `https://hackage.haskell.org/package/${config.scope[name]}`; - -const renderScope = (scope: SCOPE) => link(scope, packageURL(scope)); - -const indent = (x: number) => - range(0, x * 2) - .map(() => " ") - .join(""); - -const renderStats = (topics: [string, string][]) => - topics - .filter(([_, value]) => value) - .map(([topic, value]) => `${indent(1)}- ${topic} ${value}`) - .join("\n"); - -const renderPullRequest = ({ - number, - author, - title, - body, - scopes, -}: Change): string => { - const details = body - ? `${indent(1)}-
\n${indent(3)}${body.replace( - /\n/g, - `\n${indent(3)}` - )}\n${indent(1)}
` - : ""; - - const head = `* ${link( - `#${number}`, - github.issue(number) - )}: ${title?.trim()}`; - - const stats = renderStats([ - ["📦", scopes.map(renderScope).join(", ")], - ["👤", link(`@${author.login}`, author.url)], - ["📎", ""], - ]); - - return [head, stats, details].filter(Boolean).join("\n"); -}; - -const renderSection = (label: string, pullRequests: Change[]) => - [`#### ${label}`, pullRequests.map(renderPullRequest)].flat().join("\n"); - -const render = (tag: string, changes: Change[]) => { - const groups = groupBy(({ type }) => type, changes); - - return [ - `## ${tag || "Unreleased"} (${getDate()})\n`, - Object.entries(pullRequestTypes) - .flatMap(([type, label]) => - isKey(groups, type) ? renderSection(label, groups[type]) : "" - ) - .filter(Boolean) - .join("\n\n"), - ].join("\n"); -}; - -export { render }; diff --git a/scripts/lib/changelog/types.ts b/scripts/lib/changelog/types.ts deleted file mode 100644 index 9ef3b8738..000000000 --- a/scripts/lib/changelog/types.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Maybe } from "../types"; - -const config = { - scope: { - server: "morpheus-graphql", - client: "morpheus-graphql-client", - core: "morpheus-graphql-core", - subscriptions: "morpheus-graphql-subscriptions", - tests: "morpheus-graphql-tests", - app: "morpheus-graphql-app", - }, - pr: { - breaking: "Breaking Change", - feature: "New features", - fix: "Bug Fixes", - chore: "Minor Changes", - }, -}; - -export const pullRequestTypes = config.pr; - -type CONFIG = typeof config; -type LabelKind = keyof CONFIG; -type VALUES = keyof CONFIG[T]; - -export const parseLabel = - (kind: T) => - (label: string): Maybe> => { - const [prefix, name, ...rest] = label.split("/"); - - if (prefix !== kind) return; - - if (rest.length || !name || !(config[kind] as any)[name]) { - throw new Error(`invalid label ${label}`); - } - - return name as VALUES; - }; - -type SCOPE = keyof typeof config.scope; - -type PR_TYPE = keyof typeof pullRequestTypes; - -export { SCOPE, PR_TYPE, config }; diff --git a/scripts/lib/gh.ts b/scripts/lib/gh.ts deleted file mode 100644 index d4a25012b..000000000 --- a/scripts/lib/gh.ts +++ /dev/null @@ -1,74 +0,0 @@ -import axios from "axios"; -import { git } from "./git"; -import { chunks, hconf } from "./utils"; - -const token = () => { - const { GITHUB_TOKEN } = process.env; - - if (!GITHUB_TOKEN) { - throw new Error("missing variable: GITHUB_TOKEN"); - } - return GITHUB_TOKEN; -}; - -const gh = (path: string, body: {}) => - axios - .post(`https://api.github.com/${path}`, JSON.stringify(body), { - headers: { - authorization: `Bearer ${token()}`, - "content-type": "application/json", - accept: "Accept: application/vnd.github.v3+json", - }, - }) - .then(({ data }) => data.data) - .catch((err) => Promise.reject(err.message)); - -class Github { - constructor(private org: string, private repo: string) {} - - private get path() { - return `github.com/${this.org}/${this.repo}`; - } - - public isOwner = ({ nameWithOwner }: { nameWithOwner: string }) => - nameWithOwner === `${this.org}/${this.repo}`; - - public batch = - (f: (_: string | number) => string) => - (items: Array) => - Promise.all( - chunks(items).map((chunk) => - gh("graphql", { - query: `{ - repository(owner: "${this.org}", name: "${this.repo}") { - ${chunk.map((n) => `item_${n}:${f(n)}`).join("\n")} - } - }`, - }).then(({ repository }) => Object.values(repository)) - ) - ).then((x) => x.flat().filter(Boolean) as O[]); - - public issue = (n: number) => `https://${this.path}/issues/${n}`; - - public release = async (body: string) => { - const version = await hconf("version"); - const name = `publish-release/${version}`; - - git("add", "."); - git("status"); - git("commit", "-m", `"${name}"`); - git("push", `https://${token()}@${this.path}.git`, `HEAD:${name}`); - - return gh(`repos/${this.org}/${this.repo}/pulls`, { - head: name, - draft: true, - base: "main", - owner: this.org, - repo: this.repo, - title: `Publish Release ${version}`, - body, - }); - }; -} - -export const github = new Github("morpheusgraphql", "morpheus-graphql"); diff --git a/scripts/lib/git.ts b/scripts/lib/git.ts deleted file mode 100644 index 74ce92450..000000000 --- a/scripts/lib/git.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { exec } from "./utils"; - -const git = (...cmd: string[]) => exec(["git", ...cmd].join(" ")); - -const getDate = () => git("log", "-1", "--format=%cd", "--date=short"); -const lastTag = () => git("describe", "--abbrev=0", "--tags"); -const commitsAfter = (tag: string) => - git("rev-list", "--reverse", `${tag}..`).split("\n"); - -export { git, getDate, lastTag, commitsAfter, exec }; diff --git a/scripts/lib/types.ts b/scripts/lib/types.ts deleted file mode 100644 index fff409be8..000000000 --- a/scripts/lib/types.ts +++ /dev/null @@ -1 +0,0 @@ -export type Maybe = undefined | T; diff --git a/scripts/lib/utils.ts b/scripts/utils.ts similarity index 64% rename from scripts/lib/utils.ts rename to scripts/utils.ts index 669df2984..14970aba1 100644 --- a/scripts/lib/utils.ts +++ b/scripts/utils.ts @@ -3,27 +3,6 @@ import { writeFile } from "fs/promises"; import { dirname, join } from "path"; import * as core from "@actions/core"; -export const getPRNumber = (msg: string) => { - const num = / \(#(?[0-9]+)\)$/m.exec(msg)?.groups?.prNumber; - return num ? parseInt(num, 10) : undefined; -}; - -export const chunks = (xs: T[]): T[][] => { - const batches: T[][] = []; - - for (let i = 0; i < xs.length; i += 50) { - const batch = xs.slice(i, i + 50); - batches.push(batch); - } - - return batches; -}; - -export const isKey = ( - obj: Record, - key?: string | null -): key is T => typeof key === "string" && key in obj; - export const exit = (error: Error) => { core.setFailed(error); console.error(error); @@ -62,6 +41,3 @@ export const hconf = async ( export const write = (p: string) => (f: string) => writeFile(join(dirname(require.main?.filename ?? ""), "../", p), f, "utf8"); - -export const setOutput = (name: string) => (value: string) => - core.setOutput(name, value);