From cce3fa17e79edf9b05398ffa304196cece60f7c4 Mon Sep 17 00:00:00 2001 From: Luke Karrys Date: Thu, 29 Sep 2022 17:10:13 -0700 Subject: [PATCH] wip --- .github/workflows/release.yml | 1 + bin/release-manager.js | 261 ++++++++++++++++++++++++++++++++++ lib/content/release.yml | 1 + package.json | 4 +- 4 files changed, 266 insertions(+), 1 deletion(-) create mode 100755 bin/release-manager.js diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 01a8d6a9..aee93ddf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -132,6 +132,7 @@ jobs: RELEASE_COMMENT_ID: ${{ needs.release.outputs.comment-id }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | + npm exec --offline -- template-oss-release-manager npm run rp-pull-request --ignore-scripts -ws -iwr --if-present - name: Commit id: commit diff --git a/bin/release-manager.js b/bin/release-manager.js new file mode 100755 index 00000000..fbdbd206 --- /dev/null +++ b/bin/release-manager.js @@ -0,0 +1,261 @@ +#!/usr/bin/env node + +const { Octokit } = require('@octokit/rest') +const semver = require('semver') +const mapWorkspaces = require('@npmcli/map-workspaces') +const { join } = require('path') + +const log = (...logs) => console.error('LOG', ...logs) + +const ROOT = process.cwd() +const pkg = require(join(ROOT, 'package.json')) + +/* eslint-disable max-len */ +const DEFAULT_RELEASE_PROCESS = ` +1. Checkout the release branch and test + + \`\`\`sh + gh pr checkout --force + npm i + npm test + gh pr checks --watch + \`\`\` + +1. Publish workspaces + + \`\`\`sh + npm publish -w + \`\`\` + +1. Publish + + \`\`\`sh + npm publish + \`\`\` + +1. Merge release PR + + \`\`\`sh + gh pr merge --rebase + git checkout + git fetch + git reset --hard origin/ + \`\`\` + +1. Check For Release Tags + + Release Please will run on the just pushed release commit and create GitHub releases and tags for each package. + + \`\`\` + gh run watch \`gh run list -w release -b -L 1 --json databaseId -q ".[0].databaseId"\` + \`\`\` +` /* eslint-enable */ + +const getReleaseProcess = async ({ owner, repo }) => { + const RELEASE_LIST_ITEM = /^\d+\.\s/gm + + log(`Fetching release process from:`, owner, repo, 'wiki') + + let releaseProcess = '' + try { + releaseProcess = await new Promise((resolve, reject) => { + require('https') + .get(`https://raw.githubusercontent.com/wiki/${owner}/${repo}/Release-Process.md`, resp => { + let d = '' + resp.on('data', c => (d += c)) + resp.on('end', () => { + if (resp.statusCode !== 200) { + reject(new Error(`${resp.req.protocol + resp.req.host + resp.req.path}: ${d}`)) + } else { + resolve(d) + } + }) + }) + .on('error', reject) + }) + console.log(releaseProcess) + } catch (e) { + log('Release wiki not found', e.message) + log('Using default release process') + releaseProcess = DEFAULT_RELEASE_PROCESS.trim() + '\n' + } + + // XXX: the release steps need to always be the last thing in the doc for this to work + const releaseLines = releaseProcess.split('\n') + const releaseStartLine = releaseLines.reduce((acc, line, index) => + line.match(/^#+\s/) ? index : acc, 0) + const section = releaseLines.slice(releaseStartLine).join('\n') + + return section.split({ + [Symbol.split] (str) { + const [, ...matches] = str.split(RELEASE_LIST_ITEM) + log(`Found ${matches.length} release items`) + return matches.map((m) => `- [ ] . ${m}`.trim()) + }, + }) +} + +const getPrReleases = async (pr) => { + const RELEASE_SEPARATOR = /
.*<\/summary>/g + const MONO_VERSIONS = /
(?:(.*?):\s)?(.*?)<\/summary>/ + const ROOT_VERSION = /\n##\s\[(.*?)\]/ + + const workspaces = [...await mapWorkspaces({ pkg: pkg, cwd: ROOT })].reduce((acc, [k]) => { + const wsComponentName = k.startsWith('@') ? k.split('/')[1] : k + acc[wsComponentName] = k + return acc + }, {}) + + const getReleaseInfo = ({ name, version: rawVersion }) => { + const version = semver.parse(rawVersion) + const prerelease = !!version.prerelease.length + const tag = `${name ? `${name}-` : ''}v${rawVersion}` + const workspace = workspaces[name] + + return { + name, + tag, + prerelease, + version: rawVersion, + major: version.major, + url: `https://github.com/${pr.base.repo.full_name}/releases/tag/${tag}`, + flags: `${name ? `-w ${workspace}` : ''} ${prerelease ? `--tag prerelease` : ''}`.trim(), + } + } + + const releases = pr.body.match(RELEASE_SEPARATOR) + + if (!releases) { + log('Found no monorepo, checking for single root version') + const [, version] = pr.body.match(ROOT_VERSION) || [] + + if (!version) { + throw new Error('Could not find version with:', ROOT_VERSION) + } + + log('Found version', version) + return [getReleaseInfo({ version })] + } + + log(`Found ${releases.length} releases`) + + return releases.reduce((acc, r) => { + const [, name, version] = r.match(MONO_VERSIONS) + const release = getReleaseInfo({ name, version }) + + if (!name) { + log('Found root', release) + acc[0] = release + } else { + log('Found workspace', release) + acc[1].push(release) + } + + return acc + }, [null, []]) +} + +const appendToComment = async ({ github, commentId, title, body }) => { + if (!commentId) { + log(`No comment id, skipping append to comment`) + return + } + + const { data: comment } = await github.rest.issues.getComment({ + ...github.repo, + comment_id: commentId, + }) + + const hasAppended = comment.body.includes(title) + + log('Found comment with id:', commentId) + log(hasAppended ? 'Comment has aready been appended, replacing' : 'Appending to comment') + + const prefix = hasAppended + ? comment.body.split(title)[0] + : comment.body + + return github.rest.issues.updateComment({ + ...github.repo, + comment_id: commentId, + body: [prefix, title, body].join('\n\n'), + }) +} + +const main = async (env) => { + // These env vars are set by the release.yml workflow from template-oss + const { + CI, + GITHUB_TOKEN, + GITHUB_REPOSITORY, + RELEASE_PR_NUMBER, + RELEASE_COMMENT_ID, // comment is optional for testing + } = env + + if (!CI || !GITHUB_TOKEN || !GITHUB_REPOSITORY || !RELEASE_PR_NUMBER) { + throw new Error('This script is designed to run in CI. If you want to test it, set the ' + + `following env vars: \`CI, GITHUB_TOKEN, GITHUB_REPOSITORY, RELEASE_PR_NUMBER\``) + } + + const [owner, repo] = GITHUB_REPOSITORY.split('/') + const github = new Octokit({ auth: GITHUB_TOKEN }) + github.repo = { owner, repo } + + const { data: pr } = await github.rest.pulls.get({ + ...github.repo, + pull_number: RELEASE_PR_NUMBER, + }) + + const [release, workspaces = []] = await getPrReleases(pr) + + const RELEASE_OMIT_PRERELEASE = '> NOT FOR PRERELEASE' + const RELEASE_OMIT_WORKSPACES = 'Publish workspaces' + const releaseItems = (await getReleaseProcess({ owner, repo })) + .filter((item) => { + if (release.prerelease && item.includes(RELEASE_OMIT_PRERELEASE)) { + return false + } + + if (!workspaces.length && item.includes(RELEASE_OMIT_WORKSPACES)) { + return false + } + + return true + }) + .map((item, index) => item.replace('', index + 1)) + + log( + `Filtered ${releaseItems.length} release process items:\n`, + releaseItems.map(r => r.split('\n')[0].replace('- [ ] ', '')).join(', ') + ) + + const releaseTitle = `### Release Checklist for ${release.tag}` + const releaseChecklist = releaseItems + .join('\n\n') + .replace(//g, RELEASE_PR_NUMBER) + .replace(//g, pr.head.ref) + .replace(//g, pr.base.ref) + .replace(//g, release.major) + .replace(//g, release.version) + .replace(//g, release.url) + .replace(//g, release.flags) + .replace(/^\s+([\w].*)-w $/g, workspaces.map(w => `$1${w.flags}`).join('')) + .trim() + + await appendToComment({ + github, + commentId: RELEASE_COMMENT_ID, + title: releaseTitle, + body: releaseChecklist, + }) + + if (!RELEASE_COMMENT_ID) { + console.log(releaseChecklist) + } +} + +main(process.env) + // This is part of the release CI and is for posting a release manager + // comment to the issue but we dont want it to ever fail the workflow so + // just log but dont set the error code + .catch(err => console.error(err)) diff --git a/lib/content/release.yml b/lib/content/release.yml index a37b8e1e..cf6ade3f 100644 --- a/lib/content/release.yml +++ b/lib/content/release.yml @@ -77,6 +77,7 @@ jobs: RELEASE_COMMENT_ID: $\{{ needs.release.outputs.comment-id }} GITHUB_TOKEN: $\{{ secrets.GITHUB_TOKEN }} run: | + {{ rootNpmPath }} exec --offline -- template-oss-release-manager {{ rootNpmPath }} run rp-pull-request --ignore-scripts {{ allFlags }} - name: Commit id: commit diff --git a/package.json b/package.json index 28591aac..42bcdea7 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "bin": { "template-oss-apply": "bin/apply.js", "template-oss-check": "bin/check.js", - "template-oss-release-please": "bin/release-please.js" + "template-oss-release-please": "bin/release-please.js", + "template-oss-release-manager": "bin/release-manager.js" }, "scripts": { "lint": "eslint \"**/*.js\"", @@ -37,6 +38,7 @@ "@npmcli/git": "^3.0.0", "@npmcli/map-workspaces": "^2.0.2", "@npmcli/package-json": "^2.0.0", + "@octokit/rest": "^19.0.4", "diff": "^5.0.0", "glob": "^8.0.1", "handlebars": "^4.7.7",