diff --git a/lib/cms/uploadFolder.ts b/lib/cms/uploadFolder.ts index 17a324db..5023de6d 100644 --- a/lib/cms/uploadFolder.ts +++ b/lib/cms/uploadFolder.ts @@ -191,7 +191,7 @@ async function uploadMetaJsonFiles( uploadFile: (file: string) => () => Promise ): Promise { const moduleMetaJsonFiles = moduleFiles.filter(isMetaJsonFile); - + if (moduleMetaJsonFiles.length > 0) { await queue.addAll(moduleMetaJsonFiles.map(uploadFile)); } diff --git a/package.json b/package.json index c672ec0e..4c5828f4 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ }, "license": "Apache-2.0", "devDependencies": { + "@hubspot/npm-scripts": "0.0.4", "@inquirer/prompts": "^7.0.1", "@types/content-disposition": "^0.5.5", "@types/cors": "^2.8.15", diff --git a/scripts/release.ts b/scripts/release.ts index 8464f417..19ba1c2d 100644 --- a/scripts/release.ts +++ b/scripts/release.ts @@ -1,374 +1,15 @@ -import { exec as _exec, spawn } from 'child_process'; -import { promisify } from 'util'; -import yargs, { ArgumentsCamelCase, Argv } from 'yargs'; -import semver from 'semver'; -import open from 'open'; -import { confirm, input } from '@inquirer/prompts'; - -import { - name as packageName, - version as localVersion, - publishConfig, -} from '../package.json'; -import { logger, setLogLevel, LOG_LEVEL } from '../lib/logger'; +import { buildReleaseScript } from '@hubspot/npm-scripts/src/release'; +import path from 'path'; import { build } from './lib/build'; -const exec = promisify(_exec); - -const MAIN_BRANCH = 'main'; - -const TAG = { - LATEST: 'latest', - NEXT: 'next', - EXPERIMENTAL: 'experimental', -} as const; - -const INCREMENT = { - PATCH: 'patch', - MINOR: 'minor', - MAJOR: 'major', - PRERELEASE: 'prerelease', -} as const; - -const VERSION_INCREMENT_OPTIONS = [ - INCREMENT.PATCH, - INCREMENT.MINOR, - INCREMENT.MAJOR, - INCREMENT.PRERELEASE, -] as const; -const TAG_OPTIONS = [TAG.LATEST, TAG.NEXT, TAG.EXPERIMENTAL] as const; - -const PRERELEASE_IDENTIFIER = { - NEXT: 'beta', - EXPERIMENTAL: 'experimental', -} as const; - -const EXIT_CODES = { - SUCCESS: 0, - ERROR: 1, -}; - -// Commands run with `spawn` won't always get this from the package.json -const REGISTRY = publishConfig.registry; - -type ReleaseArguments = { - versionIncrement: (typeof VERSION_INCREMENT_OPTIONS)[number]; - tag: (typeof TAG_OPTIONS)[number]; - dryRun?: boolean; -}; - -type DistTags = { - [TAG.LATEST]: string; - [TAG.NEXT]: string; - [TAG.EXPERIMENTAL]: string; -}; - -type Tag = (typeof TAG_OPTIONS)[number]; - -async function getGitBranch(): Promise { - const { stdout } = await exec('git rev-parse --abbrev-ref HEAD'); - return stdout.trim(); -} - -async function getDistTags(): Promise { - const { stdout } = await exec(`npm view ${packageName} dist-tags --json`); - const distTags = stdout.trim(); - return JSON.parse(distTags) as DistTags; -} - -async function cleanup(newVersion: string): Promise { - await exec(`git reset HEAD~`); - await exec(`git checkout .`); - await exec(`git tag -d v${newVersion}`); -} - -async function publish( - tag: Tag, - otp: string, - isDryRun: boolean -): Promise { - logger.log(); - logger.log(`Publishing to ${tag}...`); - logger.log('-'.repeat(50)); - logger.log(); - - const commandArgs = [ - 'publish', - '--tag', - tag, - '--registry', - REGISTRY, - '--otp', - otp, - ]; - - if (isDryRun) { - commandArgs.push('--dry-run'); - } - - return new Promise((resolve, reject) => { - const childProcess = spawn('npm', commandArgs, { - stdio: 'inherit', - cwd: './dist', - }); - - childProcess.on('close', code => { - if (code !== EXIT_CODES.SUCCESS) { - reject(); - } else { - resolve(); - } - }); - }); -} - -async function updateNextTag( - newVersion: string, - otp: string, - isDryRun: boolean -): Promise { - logger.log(); - logger.log(`Updating ${TAG.NEXT} tag...`); - - const commandArgs = [ - 'dist-tag', - 'add', - `${packageName}@${newVersion}`, - TAG.NEXT, - '--registry', - REGISTRY, - '--otp', - otp, - ]; - - return new Promise((resolve, reject) => { - if (isDryRun) { - const distTagCommand = ['npm', ...commandArgs].join(' '); - logger.log(`Dry run: skipping run of \`${distTagCommand}\``); - resolve(); - } else { - const childProcess = spawn('npm', commandArgs, { stdio: 'inherit' }); - - childProcess.on('close', code => { - if (code !== EXIT_CODES.SUCCESS) { - reject(); - } else { - logger.success(`${TAG.NEXT} tag updated successfully`); - resolve(); - } - }); - } - }); -} - -async function handler({ - versionIncrement, - tag, - dryRun, -}: ArgumentsCamelCase): Promise { - setLogLevel(LOG_LEVEL.LOG); - - const branch = await getGitBranch(); - - const isExperimental = tag === TAG.EXPERIMENTAL; - const isDryRun = Boolean(dryRun); - - if (isExperimental && branch === MAIN_BRANCH) { - logger.error( - 'Releases to experimental tag cannot be published from the main branch' - ); - process.exit(EXIT_CODES.ERROR); - } else if (!isExperimental && branch !== MAIN_BRANCH) { - logger.error( - 'Releases to latest and next tags can only be published from the main branch' - ); - process.exit(EXIT_CODES.ERROR); - } - - if (tag === TAG.LATEST && versionIncrement === INCREMENT.PRERELEASE) { - logger.error( - 'Invalid release: cannot increment prerelease number on latest tag.' - ); - process.exit(EXIT_CODES.ERROR); - } - - const { next: currentNextTag, experimental: currentExperimentalTag } = - await getDistTags(); - - if (!isExperimental && currentNextTag !== localVersion) { - logger.error( - `Local package.json version ${localVersion} is out of sync with published version ${currentNextTag}` - ); - process.exit(EXIT_CODES.ERROR); - } - - const currentVersion = isExperimental ? currentExperimentalTag : localVersion; - const prereleaseIdentifier = isExperimental - ? PRERELEASE_IDENTIFIER.EXPERIMENTAL - : PRERELEASE_IDENTIFIER.NEXT; - const incrementType = - tag === TAG.LATEST || versionIncrement === INCREMENT.PRERELEASE - ? versionIncrement - : (`pre${versionIncrement}` as const); - - const newVersion = semver.inc( - currentVersion, - incrementType, - prereleaseIdentifier - ); - - if (!newVersion) { - logger.error('Error incrementing version.'); - process.exit(EXIT_CODES.ERROR); - } - - logger.log(); - if (dryRun) { - logger.log('DRY RUN'); - } - logger.log(`Current version: ${currentVersion}`); - logger.log(`New version to release: ${newVersion}`); - - const shouldRelease = await confirm({ - message: `Release version ${newVersion} on tag ${tag}?`, - }); - - if (!shouldRelease) { - process.exit(EXIT_CODES.SUCCESS); - } - - if ( - tag === TAG.LATEST && - !localVersion.includes(PRERELEASE_IDENTIFIER.NEXT) - ) { - logger.log(); - const proceedWithoutBetaRelease = await confirm({ - message: `The current changes have not yet been released in beta. It's recommended to release and test all changes on the ${TAG.NEXT} tag before releasing them to ${TAG.LATEST}. Are you sure you want to proceed?`, - default: false, - }); - - if (!proceedWithoutBetaRelease) { - logger.log(); - logger.log( - `To release your changes on the next tag, run \`yarn release -v=${versionIncrement} -t=next\`` - ); - process.exit(EXIT_CODES.SUCCESS); - } - } - - logger.log(); - logger.log(`Updating version to ${newVersion}...`); - if (isExperimental) { - await exec(`yarn version --no-git-tag-version --new-version ${newVersion}`); - } else { - await exec(`yarn version --new-version ${newVersion}`); - } - logger.success('Version updated successfully'); - - logger.log(); - await build(); - - let otp = ''; - - logger.log(); - if (!isDryRun) { - otp = await input({ message: 'Enter your NPM one-time password:' }); - } else { - logger.log('Dry run: skipping one-time password entry'); - } - - try { - await publish(tag, otp, isDryRun); - } catch (e) { - logger.error( - 'An error occurred while releasing the package. Correct the error and re-run `yarn build`.' - ); - await cleanup(newVersion); - process.exit(EXIT_CODES.ERROR); - } - - const gitCommand = `git push --atomic origin ${branch} v${newVersion}`; - - if (tag === TAG.LATEST) { - try { - await updateNextTag(newVersion, otp, isDryRun); - } catch (e) { - logger.error( - `An error occured while updating the ${TAG.NEXT} tag. To finish this release, run the following commands:` - ); - logger.log(`npm dist-tag add ${packageName}@${newVersion} ${TAG.NEXT}`); - logger.log(gitCommand); - } - } - - if (isDryRun) { - await cleanup(newVersion); - logger.log(); - logger.log('Dry run: skipping push to Github'); - logger.success('Dry run release finished successfully.'); - process.exit(EXIT_CODES.SUCCESS); - } - - if (isExperimental) { - logger.log(); - logger.log(`Experimental release: Skipping push to Github`); - - // Reset the version back to the local version - await exec( - `yarn version --no-git-tag-version --new-version ${localVersion}` - ); - } else { - logger.log(); - logger.log(`Pushing changes to Github...`); - await exec(gitCommand); - logger.log(`Changes pushed successfully`); - } - - logger.log(); - logger.success( - `@hubspot/local-dev-lib version ${newVersion} published successfully` - ); - logger.log( - 'View on npm: https://www.npmjs.com/package/@hubspot/local-dev-lib?activeTab=versions' - ); - - if (tag === TAG.LATEST) { - logger.log(); - logger.log('Remember to create a new release on Github!'); - open('https://github.com/HubSpot/hubspot-local-dev-lib/releases/new'); - } -} - -async function builder(yargs: Argv): Promise { - return yargs.options({ - versionIncrement: { - alias: 'v', - demandOption: true, - describe: 'SemVer increment type for the next release', - choices: VERSION_INCREMENT_OPTIONS, - }, - tag: { - alias: 't', - demandOption: true, - describe: 'Tag for the next release', - choices: TAG_OPTIONS, - }, - dryRun: { - alias: 'd', - describe: 'Run through the publish process without actually publishing', - type: 'boolean', - }, - }); -} - -yargs(process.argv.slice(2)) - .scriptName('yarn') - .usage('Release script') - .command( - 'release', - 'Create a new npm release of local-dev-lib with the specified version and tag', - builder, - handler - ) - .version(false) - .help().argv; +const packageJsonLocation = path.resolve( + path.join(__dirname, '..', 'package.json') +); + +buildReleaseScript({ + packageJsonLocation, + buildHandlerOptions: { + repositoryUrl: 'https://github.com/HubSpot/hubspot-local-dev-lib', + build, + }, +}); diff --git a/utils/cms/modules.ts b/utils/cms/modules.ts index 33667325..386feae9 100644 --- a/utils/cms/modules.ts +++ b/utils/cms/modules.ts @@ -51,4 +51,3 @@ export function isModuleFolderChild( .slice(0, length - 1) .some(part => isModuleFolder({ ...pathInput, path: part })); } -