diff --git a/package.json b/package.json index afa75f8de9..df53d55fc2 100644 --- a/package.json +++ b/package.json @@ -25,11 +25,11 @@ "docs:deploy": "./scripts/docs/deployDocs.sh", "docs:build": "node scripts/docs/buildDocs.js", "docs:start": "(cd docuilib && yarn start)", - "release": "./scripts/release/releaseUiLib.sh", + "release": "node ./scripts/release/release.js", "releaseDemo": "./scripts/release/releaseDemo.sh", "releaseDocs": "./scripts/release/releaseDocs.sh", "releaseEslint": "./scripts/release/releaseEslint.sh", - "releaseNative": "./scripts/release/releaseNative.sh" + "releaseNative": "" }, "workspaces": { "packages": [ diff --git a/packages/react-native-ui-lib/package.json b/packages/react-native-ui-lib/package.json index bedc21dfef..721aa390f4 100644 --- a/packages/react-native-ui-lib/package.json +++ b/packages/react-native-ui-lib/package.json @@ -25,7 +25,7 @@ "xcode": "xed ios", "build": "node scripts/build/build.js", "build:local": "./scripts/build/createLocalPackage.sh", - "release": "node ./scripts/release/release.js" + "release": "" }, "dependencies": { "babel-plugin-transform-inline-environment-variables": "^0.0.2", diff --git a/packages/react-native-ui-lib/scripts/release/release.js b/packages/react-native-ui-lib/scripts/release/release.js deleted file mode 100644 index 543401a224..0000000000 --- a/packages/react-native-ui-lib/scripts/release/release.js +++ /dev/null @@ -1,100 +0,0 @@ -const exec = require('shell-utils').exec; -const cp = require('child_process'); -const semver = require('semver'); -const _ = require('lodash'); -const p = require('path'); - -// Workaround JS - -// Export buildkite variables for Release build -// We cast toString() because function returns 'object' -const isRelease = process.env.BUILDKITE_MESSAGE?.match?.(/^release$/i); -let VERSION; -if (isRelease) { - VERSION = cp.execSync(`buildkite-agent meta-data get version`).toString(); -} - -const VERSION_TAG = isRelease ? 'latest' : 'snapshot'; -const VERSION_INC = 'patch'; -function run() { - console.log('Release UI Lib'); - if (!validateEnv()) { - console.log('Not a valid environment to release in!'); - return; - } - - console.log('Valid environment - releasing...'); - setupGit(); - createNpmRc(); - versionTagAndPublish(); -} - -function validateEnv() { - if (!process.env.CI) { - throw new Error('releasing is only available from CI'); - } - return ( - process.env.BUILDKITE_BRANCH === 'master' || - process.env.BUILDKITE_BRANCH === 'release' || - process.env.BUILDKITE_MESSAGE === 'snapshot' - ); -} - -function setupGit() { - exec.execSyncSilent('git config --global push.default simple'); - exec.execSyncSilent(`git config --global user.email "${process.env.GIT_EMAIL}"`); - exec.execSyncSilent(`git config --global user.name "${process.env.GIT_USER}"`); - const remoteUrl = new RegExp('https?://(\\S+)').exec(exec.execSyncRead('git remote -v'))[1]; - exec.execSyncSilent(`git remote add deploy "https://${process.env.GIT_USER}:${process.env.GIT_TOKEN}@${remoteUrl}"`); - // exec.execSync(`git checkout ${ONLY_ON_BRANCH}`); -} - -function createNpmRc() { - exec.execSync('rm -f package-lock.json'); - const npmrcPath = p.resolve(`${__dirname}/.npmrc`); - exec.execSync(`cp -rf ${npmrcPath} ${process.env.HOME}/.npmrc`); -} - -function versionTagAndPublish() { - const currentPublished = findCurrentPublishedVersion(); - console.log(`current published version: ${currentPublished}`); - - const version = isRelease ? VERSION : `${currentPublished}-snapshot.${process.env.BUILDKITE_BUILD_NUMBER}`; - console.log(`Publishing version: ${version}`); - - tryPublishAndTag(version); -} - -function findCurrentPublishedVersion() { - return exec.execSyncRead(`npm view ${process.env.npm_package_name} dist-tags.latest`); -} - -function tryPublishAndTag(version) { - let theCandidate = version; - for (let retry = 0; retry < 5; retry++) { - try { - tagAndPublish(theCandidate); - console.log(`Released ${theCandidate}`); - return; - } catch (err) { - const alreadyPublished = _.includes(err.toString(), 'You cannot publish over the previously published version'); - if (!alreadyPublished) { - throw err; - } - console.log(`previously published. retrying with increased ${VERSION_INC}...`); - theCandidate = semver.inc(theCandidate, VERSION_INC); - } - } -} - -function tagAndPublish(newVersion) { - console.log(`trying to publish ${newVersion}...`); - exec.execSync(`npm --no-git-tag-version version ${newVersion}`); - exec.execSync(`npm publish --tag ${VERSION_TAG} --workspace react-native-ui-lib`); - if (isRelease) { - exec.execSync(`git tag -a ${newVersion} -m "${newVersion}"`); - } - exec.execSyncSilent(`git push deploy ${newVersion} || true`); -} - -run(); diff --git a/packages/uilib-native/package.json b/packages/uilib-native/package.json index 7950cc8bb5..22ed5229a6 100644 --- a/packages/uilib-native/package.json +++ b/packages/uilib-native/package.json @@ -1,22 +1,22 @@ { - "name": "uilib-native", - "version": "5.1.0", - "homepage": "https://github.com/wix/react-native-ui-lib", - "description": "uilib native components (separated from js components)", - "main": "components/index", - "scripts": { - "releaseNative": "node ./scripts/releaseNative.js" - }, - "author": "Ethan Sharabi ", - "license": "MIT", - "dependencies": { - "lodash": "^4.17.21" - }, - "devDependencies": { - "shell-utils": "^1.0.10" - }, - "peerDependencies": { - "react": ">=19.0.0", - "react-native": ">=0.78.3" - } + "name": "uilib-native", + "version": "5.1.0", + "homepage": "https://github.com/wix/react-native-ui-lib", + "description": "uilib native components (separated from js components)", + "main": "components/index", + "scripts": { + "releaseNative": "" + }, + "author": "Ethan Sharabi ", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21" + }, + "devDependencies": { + "shell-utils": "^1.0.10" + }, + "peerDependencies": { + "react": ">=19.0.0", + "react-native": ">=0.78.3" + } } diff --git a/packages/uilib-native/scripts/.npmrc b/packages/uilib-native/scripts/.npmrc deleted file mode 100644 index 1727810128..0000000000 --- a/packages/uilib-native/scripts/.npmrc +++ /dev/null @@ -1,2 +0,0 @@ -email=${NPM_EMAIL} -//registry.npmjs.org/:_authToken=${NPM_TOKEN} diff --git a/packages/uilib-native/scripts/releaseNative.js b/packages/uilib-native/scripts/releaseNative.js deleted file mode 100644 index f41b669071..0000000000 --- a/packages/uilib-native/scripts/releaseNative.js +++ /dev/null @@ -1,72 +0,0 @@ -const exec = require('shell-utils').exec; -const p = require('path'); - -// Export buildkite variables for Release build -// We cast toString() because function returns 'object' -const IS_SNAPSHOT = process.env.BUILDKITE_MESSAGE?.match(/^snapshot$/i); -const VERSION_TAG = IS_SNAPSHOT ? 'snapshot' : 'latest'; - -function run() { - if (!validateEnv()) { - console.log('Do not release native'); - return; - } - - console.log('Release native'); - const packageJsonVersion = require('../package.json').version; - const currentPublished = findCurrentPublishedVersion(); - const newVersion = IS_SNAPSHOT - ? `${packageJsonVersion}-snapshot.${process.env.BUILDKITE_BUILD_NUMBER}` - : packageJsonVersion; - - if (currentPublished !== packageJsonVersion) { - createNpmRc(); - versionTagAndPublish(currentPublished, newVersion); - } -} - -function validateEnv() { - if (!process.env.CI) { - throw new Error('releasing is only available from CI'); - } - return process.env.BUILDKITE_BRANCH === 'master' || process.env.BUILDKITE_MESSAGE === 'snapshot'; -} - -function createNpmRc() { - exec.execSync('rm -f package-lock.json'); - const npmrcPath = p.resolve(`${__dirname}/.npmrc`); - exec.execSync(`cp -rf ${npmrcPath} .`); -} - -function versionTagAndPublish(currentPublished, newVersion) { - console.log(`current published version: ${currentPublished}`); - console.log(`Publishing version: ${newVersion}`); - tryPublishAndTag(newVersion); -} - -function findCurrentPublishedVersion() { - return exec.execSyncRead(`npm view ${process.env.npm_package_name} dist-tags.latest`); -} - -function tryPublishAndTag(version) { - try { - tagAndPublish(version); - console.log(`Released ${version}`); - } catch (err) { - console.log(`Failed to release ${version}`, err); - } -} - -function tagAndPublish(newVersion) { - console.log(`trying to publish ${newVersion}...`); - if (IS_SNAPSHOT) { - exec.execSync(`npm --no-git-tag-version version ${newVersion}`); - } - exec.execSync(`npm publish --tag ${VERSION_TAG} --workspace uilib-native`); - if (!IS_SNAPSHOT) { - exec.execSync(`git tag -a ${newVersion} -m "${newVersion}"`); - } - exec.execSyncSilent(`git push deploy ${newVersion} || true`); -} - -run(); diff --git a/packages/react-native-ui-lib/scripts/release/.npmrc b/scripts/release/.npmrc similarity index 100% rename from packages/react-native-ui-lib/scripts/release/.npmrc rename to scripts/release/.npmrc diff --git a/scripts/release/release.js b/scripts/release/release.js new file mode 100644 index 0000000000..dd49f24f66 --- /dev/null +++ b/scripts/release/release.js @@ -0,0 +1,143 @@ +/* eslint-disable no-restricted-syntax */ +const exec = require('shell-utils').exec; +const fs = require('fs'); +const path = require('path'); +const {logDebug, logGreen, logError} = require('../utils'); +const { + dryRun, + isRelease, + isSnapshot, + versionTag, + getPublishedVersion, + getPackageJsonVersion, + getShouldRelease, + getVersion, + setupGit, + createNpmRc +} = require('./releaseUtils'); + +if (!process.env.CI) { + logError('Releasing is only available from CI'); + if (dryRun) { + logDebug('Dry run - not exiting'); + } else { + process.exit(1); + } +} + +const PACKAGES = [ + { + name: 'uilib-native', + shouldUpdatePackageJson: !!isSnapshot, + releaseVersionStrategy: 'packageJsonVersion' + }, + { + name: 'react-native-ui-lib', + shouldUpdatePackageJson: true, + releaseVersionStrategy: isRelease ? 'buildKiteVersion' : 'packageJsonVersion', + workspaceDeps: ['uilib-native'] + } +]; + +logDebug('Checking if packages should be released...'); +PACKAGES.forEach(package => { + package.publishedVersion = getPublishedVersion(package.name); + package.packageJsonVersion = getPackageJsonVersion(package.name); + package.shouldRelease = getShouldRelease(package); +}); + +if (!PACKAGES.every(package => package.shouldRelease)) { + logGreen('No packages to release'); + if (dryRun) { + logDebug('Dry run - not exiting'); + } else { + process.exit(0); + } +} + +logDebug('Getting packages information...'); +PACKAGES.forEach(package => { + package.path = `packages/${package.name}`; + package.version = getVersion(package, dryRun); + if (package.workspaceDeps?.length > 0) { + package.workSpaceTempDeps = []; + package.workspaceDeps.forEach(dep => { + package.workSpaceTempDeps.push(PACKAGES.find(p => p.name === dep)?.publishedVersion); + }); + } +}); + +logDebug('Packages information:'); +logDebug(JSON.stringify(PACKAGES, null, 2)); + +setupGit(dryRun); +createNpmRc(dryRun); + +logGreen('Replacing all workspace:* dependencies with temporary versions...'); +for (const package of PACKAGES) { + if (package.workspaceDeps?.length > 0) { + package.workspaceDeps.forEach((dep, index) => { + const packageJsonPath = path.join(package.path, 'package.json'); + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + logDebug(`${package.name} - ${dep}:${packageJson.devDependencies[dep]} -> ${package.workSpaceTempDeps[index]}`); + packageJson.devDependencies[dep] = package.workSpaceTempDeps[index]; + fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 4) + '\n'); + }); + } +} + +const originalCwd = process.cwd(); +logDebug(`Starting release process with ${versionTag}`); +try { + for (const package of PACKAGES) { + logDebug(`Trying to release ${package.name} in ${package.path}`); + process.chdir(package.path); + // Update version in package.json + if (package.shouldUpdatePackageJson) { + if (dryRun) { + exec.execSync(`npm --no-git-tag-version --no-workspaces-update version ${package.version}`, true); + } else { + exec.execSync(`npm --no-git-tag-version version ${package.version}`, true); + } + logDebug(`Successfully updated version for ${package.name} to ${package.version}`); + } else { + logDebug(`Skipping version update for ${package.name}`); + } + if (!dryRun) { + // Publish to npm + exec.execSync(`npm publish --tag ${versionTag}`); + // Create git tag for releases (not snapshots) + if (isRelease) { + exec.execSync(`git tag -a ${package.version} -m "${package.version}"`); + exec.execSync(`git push deploy ${package.version}`); + // TODO: backup - exec.execSyncSilent(`git push deploy ${package.version} || true`); + } + logGreen(`Successfully released ${package.name}@${package.version}`); + } else { + logGreen(`Dry run - not releasing ${package.name}@${package.version}`); + } + process.chdir(originalCwd); + // Write package.version to all relevant workspaceDeps + PACKAGES.forEach(p => { + if (p.workspaceDeps?.length > 0) { + p.workspaceDeps.forEach(dep => { + const packageJsonPath = path.join(originalCwd, 'packages', p.name, 'package.json'); + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + if (dep === package.name) { + const depPackageJsonPath = path.join(originalCwd, 'packages', dep, 'package.json'); + const depPackageJson = JSON.parse(fs.readFileSync(depPackageJsonPath, 'utf8')); + const oldVersion = packageJson.devDependencies[dep]; + const newVersion = depPackageJson.version; + logGreen(`Updating workspace dependency ${p.name} - ${dep}:${oldVersion} -> ${newVersion}`); + packageJson.devDependencies[dep] = newVersion; + fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 4) + '\n'); + logGreen('Successfully updated workspace dependency'); + } + }); + } + }); + } +} catch (error) { + logError(`Failed to release all packages: ${error.message}`); + process.exit(1); +} diff --git a/scripts/release/releaseNative.sh b/scripts/release/releaseNative.sh deleted file mode 100755 index 63174e6c32..0000000000 --- a/scripts/release/releaseNative.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -pushd packages/uilib-native -npm install -npm run releaseNative -popd diff --git a/scripts/release/releaseUiLib.sh b/scripts/release/releaseUiLib.sh deleted file mode 100755 index aee5335dfb..0000000000 --- a/scripts/release/releaseUiLib.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -pushd packages/react-native-ui-lib -npm install -npm run release -popd diff --git a/scripts/release/releaseUtils.js b/scripts/release/releaseUtils.js new file mode 100644 index 0000000000..20c9797220 --- /dev/null +++ b/scripts/release/releaseUtils.js @@ -0,0 +1,112 @@ +const exec = require('shell-utils').exec; +const semver = require('semver'); +const childProcess = require('child_process'); +const path = require('path'); +const {logDebug, logGreen} = require('../utils'); +const rootDir = path.resolve(__dirname, '../..'); + +let dryRun = + (process.argv?.find(arg => arg.toLowerCase().includes('-dry'))?.length ?? 0) > 0 || + (process.argv?.find(arg => arg.toLowerCase().includes('-dry-run'))?.length ?? 0) > 0 || + (process.argv?.find(arg => arg.toLowerCase().includes('-dryrun'))?.length ?? 0) > 0 || + (process.argv?.find(arg => arg.toLowerCase().includes('-d'))?.length ?? 0) > 0; + +const testRelease = + (process.argv?.find(arg => arg.toLowerCase().includes('-release'))?.length ?? 0) > 0 || + (process.argv?.find(arg => arg.toLowerCase().includes('-r'))?.length ?? 0) > 0; + +const testMaster = + (process.argv?.find(arg => arg.toLowerCase().includes('-master'))?.length ?? 0) > 0 || + (process.argv?.find(arg => arg.toLowerCase().includes('-m'))?.length ?? 0) > 0; + +let testSnapshot = + (process.argv?.find(arg => arg.toLowerCase().includes('-snapshot'))?.length ?? 0) > 0 || + (process.argv?.find(arg => arg.toLowerCase().includes('-snap'))?.length ?? 0) > 0 || + (process.argv?.find(arg => arg.toLowerCase().includes('-s'))?.length ?? 0) > 0; + +if (testRelease || testMaster || testSnapshot) { + dryRun = true; +} + +if (dryRun && !testMaster && !testRelease) { + testSnapshot = true; +} + +const isMaster = dryRun ? testMaster : process.env.BUILDKITE_BRANCH === 'master'; +const isRelease = dryRun ? testRelease : process.env.BUILDKITE_MESSAGE?.match?.(/^release$/i); +const isSnapshot = dryRun ? testSnapshot : process.env.BUILDKITE_MESSAGE?.match?.(/^snapshot$/i); +const versionTag = isRelease ? 'latest' : 'snapshot'; + +logGreen(`Starting monorepo release process${dryRun ? ' (dry run' : ''}${isMaster ? ' for master' : ''}${isRelease ? ' for release' : ''}${isSnapshot ? ' for snapshot' : ''}${dryRun ? ')' : ''}`); + +function getPublishedVersion(packageName, tag = versionTag, silent = false) { + if (!silent) { + logDebug(`Getting published version for ${packageName} with tag ${tag}`); + } + return exec.execSyncRead(`npm view ${packageName} dist-tags.${tag}`, true); +} + +function getPackageJsonVersion(packageName) { + return require(path.join(rootDir, `packages/${packageName}/package.json`)).version; +} + +function getShouldRelease(package) { + return isMaster || isRelease ? semver.gt(package.packageJsonVersion, package.publishedVersion) : !!isSnapshot; +} + +function getVersion(package, dryRun) { + let releaseVersion; + switch (package.releaseVersionStrategy) { + case 'packageJsonVersion': + releaseVersion = package.packageJsonVersion; + break; + case 'buildKiteVersion': + releaseVersion = dryRun + ? `${semver.inc(package.packageJsonVersion, 'patch')}-dry-run` + : childProcess.execSync(`buildkite-agent meta-data get version`).toString(); + break; + } + const latestVersion = getPublishedVersion(package.name, 'latest', true); + const snapshotVersion = `${latestVersion}-snapshot.${process.env.BUILDKITE_BUILD_NUMBER}`; + return isRelease ? releaseVersion : snapshotVersion; +} + +function setupGit(dryRun) { + if (dryRun) { + logDebug('Dry run - not setting up git configuration'); + return; + } + logGreen('Setting up git configuration...'); + exec.execSyncSilent('git config --global push.default simple'); + exec.execSyncSilent(`git config --global user.email "${process.env.GIT_EMAIL}"`); + exec.execSyncSilent(`git config --global user.name "${process.env.GIT_USER}"`); + const remoteUrl = new RegExp('https?://(\\S+)').exec(exec.execSyncRead('git remote -v'))[1]; + exec.execSyncSilent(`git remote add deploy "https://${process.env.GIT_USER}:${process.env.GIT_TOKEN}@${remoteUrl}"`); + logGreen('Git configured'); +} + +function createNpmRc(dryRun) { + if (dryRun) { + logDebug('Dry run - not setting up npm configuration'); + return; + } + logGreen('Setting up npm configuration...'); + exec.execSync('rm -f package-lock.json'); + const npmrcPath = path.resolve(`${__dirname}/.npmrc`); + exec.execSync(`cp -rf ${npmrcPath} .`); + logGreen('npmrc configured'); +} + +module.exports = { + dryRun, + isMaster, + isRelease, + isSnapshot, + versionTag, + getPublishedVersion, + getPackageJsonVersion, + getShouldRelease, + getVersion, + setupGit, + createNpmRc +};