UNPKG

@polkadot/dev

Version:

A collection of shared CI scripts and development environment used by @polkadot projects

537 lines (434 loc) • 11.6 kB
#!/usr/bin/env node // Copyright 2017-2025 @polkadot/dev authors & contributors // SPDX-License-Identifier: Apache-2.0 import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import process from 'node:process'; import yargs from 'yargs'; import { copyDirSync, copyFileSync, denoCreateDir, execGit, execPm, execSync, exitFatal, GITHUB_REPO, GITHUB_TOKEN_URL, gitSetup, logBin, mkdirpSync, rimrafSync, topoSort } from './util.mjs'; /** @typedef {Record<string, any>} ChangelogMap */ logBin('polkadot-ci-ghact-build'); const DENO_REPO = 'polkadot-js/build-deno.land'; const BUND_REPO = 'polkadot-js/build-bundle'; const repo = `${GITHUB_TOKEN_URL}/${GITHUB_REPO}.git`; const denoRepo = `${GITHUB_TOKEN_URL}/${DENO_REPO}.git`; const bundRepo = `${GITHUB_TOKEN_URL}/${BUND_REPO}.git`; const bundClone = 'build-bundle-clone'; const denoClone = 'build-deno-clone'; let withDeno = false; let withBund = false; let withNpm = false; /** @type {string[]} */ const shouldDeno = []; /** @type {string[]} */ const shouldBund = []; const argv = await yargs(process.argv.slice(2)) .options({ 'skip-beta': { description: 'Do not increment as beta', type: 'boolean' } }) .strict() .argv; /** * Removes a specific file, returning true if found, false otherwise * * @param {string} file * @returns {boolean} */ function rmFile (file) { if (fs.existsSync(file)) { rimrafSync(file); return true; } return false; } /** * Retrieves the path of the root package.json * * @returns {string} */ function npmGetJsonPath () { return path.resolve(process.cwd(), 'package.json'); } /** * Retrieves the contents of the root package.json * * @returns {{ name: string; version: string; versions?: { npm?: string; git?: string } }} */ function npmGetJson () { return JSON.parse( fs.readFileSync(npmGetJsonPath(), 'utf8') ); } /** * Writes the contents of the root package.json * * @param {any} json */ function npmSetJson (json) { fs.writeFileSync(npmGetJsonPath(), `${JSON.stringify(json, null, 2)}\n`); } /** * Retrieved the current version included in package.json * * @returns {string} */ function npmGetVersion () { return npmGetJson().version; } /** * Sets the current to have an -x version specifier (aka beta) */ function npmAddVersionX () { const json = npmGetJson(); if (!json.version.endsWith('-x')) { json.version = json.version + '-x'; npmSetJson(json); } } /** * Removes the current -x version specifier (aka beta) */ function npmDelVersionX () { const json = npmGetJson(); if (json.version.endsWith('-x')) { json.version = json.version.replace('-x', ''); npmSetJson(json); } } /** * Sets the {versions: { npm, git } } fields in package.json */ function npmSetVersionFields () { const json = npmGetJson(); if (!json.versions) { json.versions = {}; } json.versions.git = json.version; if (!json.version.endsWith('-x')) { json.versions.npm = json.version; } npmSetJson(json); rmFile('.123current'); } /** * Sets the npm token in the home directory */ function npmSetup () { const registry = 'registry.npmjs.org'; fs.writeFileSync(path.join(os.homedir(), '.npmrc'), `//${registry}/:_authToken=${process.env['NPM_TOKEN']}`); } /** * Publishes the current package * * @returns {void} */ function npmPublish () { if (fs.existsSync('.skip-npm') || !withNpm) { return; } ['LICENSE', 'package.json'] .filter((file) => !fs.existsSync(path.join(process.cwd(), 'build', file))) .forEach((file) => copyFileSync(file, 'build')); process.chdir('build'); const tag = npmGetVersion().includes('-') ? '--tag beta' : ''; let count = 1; while (true) { try { execSync(`npm publish --quiet --access public ${tag}`); break; } catch { if (count < 5) { const end = Date.now() + 15000; console.error(`Publish failed on attempt ${count}/5. Retrying in 15s`); count++; while (Date.now() < end) { // just spin our wheels } } } } process.chdir('..'); } /** * Creates a map of changelog entries * * @param {string[][]} parts * @param {ChangelogMap} result * @returns {ChangelogMap} */ function createChangelogMap (parts, result = {}) { for (let i = 0, count = parts.length; i < count; i++) { const [n, ...e] = parts[i]; if (!result[n]) { if (e.length) { result[n] = createChangelogMap([e]); } else { result[n] = { '': {} }; } } else { if (e.length) { createChangelogMap([e], result[n]); } else { result[n][''] = {}; } } } return result; } /** * Creates an array of changelog entries * * @param {ChangelogMap} map * @returns {string[]} */ function createChangelogArr (map) { const result = []; const entries = Object.entries(map); for (let i = 0, count = entries.length; i < count; i++) { const [name, imap] = entries[i]; if (name) { if (imap['']) { result.push(name); } const inner = createChangelogArr(imap); if (inner.length === 1) { result.push(`${name}-${inner[0]}`); } else if (inner.length) { result.push(`${name}-{${inner.join(', ')}}`); } } } return result; } /** * Adds changelog entries * * @param {string[]} changelog * @returns {string} */ function addChangelog (changelog) { const [version, ...names] = changelog; const entry = `${ createChangelogArr( createChangelogMap( names .sort() .map((n) => n.split('-')) ) ).join(', ') } ${version}`; const newInfo = `## master\n\n- ${entry}\n`; if (!fs.existsSync('CHANGELOG.md')) { fs.writeFileSync('CHANGELOG.md', `# CHANGELOG\n\n${newInfo}`); } else { const md = fs.readFileSync('CHANGELOG.md', 'utf-8'); fs.writeFileSync('CHANGELOG.md', md.includes('## master\n\n') ? md.replace('## master\n\n', newInfo) : md.replace('# CHANGELOG\n\n', `# CHANGELOG\n\n${newInfo}\n`) ); } return entry; } /** * * @param {string} repo * @param {string} clone * @param {string[]} names */ function commitClone (repo, clone, names) { if (names.length) { process.chdir(clone); const entry = addChangelog(names); gitSetup(); execGit('add --all .'); execGit(`commit --no-status --quiet -m "${entry}"`); execGit(`push ${repo}`, true); process.chdir('..'); } } /** * Publishes a specific package to polkadot-js bundles * * @returns {void} */ function bundlePublishPkg () { const { name, version } = npmGetJson(); const dirName = name.split('/')[1]; const bundName = `bundle-polkadot-${dirName}.js`; const srcPath = path.join('build', bundName); const dstDir = path.join('../..', bundClone); if (!fs.existsSync(srcPath)) { return; } console.log(`\n *** bundle ${name}`); if (shouldBund.length === 0) { shouldBund.push(version); } shouldBund.push(dirName); rimrafSync(path.join(dstDir, bundName)); copyFileSync(srcPath, dstDir); } /** * Publishes all packages to polkadot-js bundles * * @returns {void} */ function bundlePublish () { const { version } = npmGetJson(); if (!withBund && version.includes('-')) { return; } execGit(`clone ${bundRepo} ${bundClone}`, true); loopFunc(bundlePublishPkg); commitClone(bundRepo, bundClone, shouldBund); } /** * Publishes a specific package to Deno * * @returns {void} */ function denoPublishPkg () { const { name, version } = npmGetJson(); if (fs.existsSync('.skip-deno') || !fs.existsSync('build-deno')) { return; } console.log(`\n *** deno ${name}`); const dirName = denoCreateDir(name); const denoPath = `../../${denoClone}/${dirName}`; if (shouldDeno.length === 0) { shouldDeno.push(version); } shouldDeno.push(dirName); rimrafSync(denoPath); mkdirpSync(denoPath); copyDirSync('build-deno', denoPath); } /** * Publishes all packages to Deno * * @returns {void} */ function denoPublish () { const { version } = npmGetJson(); if (!withDeno && version.includes('-')) { return; } execGit(`clone ${denoRepo} ${denoClone}`, true); loopFunc(denoPublishPkg); commitClone(denoRepo, denoClone, shouldDeno); } /** * Retrieves flags based on current specifications */ function getFlags () { withDeno = rmFile('.123deno'); withBund = rmFile('.123bundle'); withNpm = rmFile('.123npm'); } /** * Bumps the current version, also applying to all sub-packages */ function verBump () { const { version: currentVersion, versions } = npmGetJson(); const [version, tag] = currentVersion.split('-'); const [,, patch] = version.split('.'); const lastVersion = versions?.npm || currentVersion; if (argv['skip-beta'] || patch === '0') { // don't allow beta versions execPm('polkadot-dev-version patch'); withNpm = true; } else if (tag || currentVersion === lastVersion) { // if we don't want to publish, add an X before passing if (!withNpm) { npmAddVersionX(); } else { npmDelVersionX(); } // beta version, just continue the stream of betas execPm('polkadot-dev-version pre'); } else { // manually set, got for publish withNpm = true; } // always ensure we have made some changes, so we can commit npmSetVersionFields(); rmFile('.123trigger'); execPm('polkadot-dev-contrib'); execGit('add --all .'); } /** * Commits and pushes the current version on git */ function gitPush () { const version = npmGetVersion(); let doGHRelease = false; if (process.env['GH_RELEASE_GITHUB_API_TOKEN']) { const changes = fs.readFileSync('CHANGELOG.md', 'utf8'); if (changes.includes(`## ${version}`)) { doGHRelease = true; } else if (version.endsWith('.1')) { exitFatal(`Unable to release, no CHANGELOG entry for ${version}`); } } execGit('add --all .'); if (fs.existsSync('docs/README.md')) { execGit('add --all -f docs'); } // add the skip checks for GitHub ... execGit(`commit --no-status --quiet -m "[CI Skip] ${version.includes('-x') ? 'bump' : 'release'}/${version.includes('-') ? 'beta' : 'stable'} ${version} skip-checks: true"`); execGit(`push ${repo} HEAD:${process.env['GITHUB_REF']}`, true); if (doGHRelease) { const files = process.env['GH_RELEASE_FILES'] ? `--assets ${process.env['GH_RELEASE_FILES']}` : ''; execPm(`polkadot-exec-ghrelease --draft ${files} --yes`); } } /** * Loops through the packages/* (or root), executing the supplied * function for each package found * * @param {() => unknown} fn */ function loopFunc (fn) { if (fs.existsSync('packages')) { const dirs = fs .readdirSync('packages') .filter((dir) => { const pkgDir = path.join(process.cwd(), 'packages', dir); return fs.statSync(pkgDir).isDirectory() && fs.existsSync(path.join(pkgDir, 'package.json')) && fs.existsSync(path.join(pkgDir, 'build')); }); topoSort(dirs) .forEach((dir) => { process.chdir(path.join('packages', dir)); fn(); process.chdir('../..'); }); } else { fn(); } } // first do infrastructure setup gitSetup(); npmSetup(); // get flags immediate, then adjust getFlags(); verBump(); // perform the actual CI build execPm('polkadot-dev-clean-build'); execPm('lint'); execPm('test'); execPm('build'); // publish to all GH repos gitPush(); denoPublish(); bundlePublish(); // publish to npm loopFunc(npmPublish);