From 0a2db60208476976537ec83a152294f2bc8f9b46 Mon Sep 17 00:00:00 2001 From: Patryk Tomczyk <13100280+patzick@users.noreply.github.com> Date: Sun, 10 May 2020 14:44:27 +0200 Subject: [PATCH] feat: add automatization to releases (#724) * feat: new release script * feat: release canary version * feat: run custom package build if there is a script for that --- .github/workflows/deploy-storefrontcloud.yml | 7 +- .github/workflows/publish-canary.yml | 9 +- .github/workflows/release-tag.yml | 26 ++ jest.config.js | 13 +- package.json | 7 +- packages/cli/package.json | 6 +- packages/cli/src/types.ts | 4 + packages/default-theme/package.json | 3 +- packages/nuxt-module/package.json | 3 +- scripts/bootstrap.js | 8 +- scripts/build.js | 6 + scripts/release.js | 253 +++++++++++++++++++ scripts/utils.js | 8 +- yarn.lock | 25 +- 14 files changed, 336 insertions(+), 42 deletions(-) create mode 100644 .github/workflows/release-tag.yml create mode 100644 scripts/release.js diff --git a/.github/workflows/deploy-storefrontcloud.yml b/.github/workflows/deploy-storefrontcloud.yml index 861986453..d1437ff7d 100644 --- a/.github/workflows/deploy-storefrontcloud.yml +++ b/.github/workflows/deploy-storefrontcloud.yml @@ -1,7 +1,8 @@ name: Deploy to Storefrontcloud on: - release: - types: [published] + push: + tags: + - "v*" # Push events to matching v*, i.e. v1.0, v20.15.10 env: RELEASE_URL: https://shopware-pwa.storefrontcloud.io INSTANCE_CODE: shopware-pwa @@ -14,7 +15,7 @@ jobs: run: | mkdir test-project cd ./test-project - npx @shopware-pwa/cli@canary init --u ${{ secrets.SHOPWARE_ADMIN_USER }} --p ${{ secrets.SHOPWARE_ADMIN_PASSWORD }} --ci --devMode + npx @shopware-pwa/cli init --u ${{ secrets.SHOPWARE_ADMIN_USER }} --p ${{ secrets.SHOPWARE_ADMIN_PASSWORD }} --ci --devMode yarn build - name: Build and publish docker image uses: elgohr/Publish-Docker-Github-Action@master diff --git a/.github/workflows/publish-canary.yml b/.github/workflows/publish-canary.yml index 3f65c8bee..bbc46b723 100644 --- a/.github/workflows/publish-canary.yml +++ b/.github/workflows/publish-canary.yml @@ -19,18 +19,15 @@ jobs: with: node-version: "12.x" registry-url: https://registry.npmjs.org/ - - name: Build packages - run: | - yarn --frozen-lockfile - yarn build --release - cd ./packages/cli && yarn build && cd ../../ - name: Publish canary version run: | - yarn lerna publish --canary --force-publish --yes --preid prealpha --dist-tag canary + yarn --frozen-lockfile + yarn release --canary env: NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} build: runs-on: ubuntu-latest + needs: publish steps: - name: Create default-theme project run: | diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml new file mode 100644 index 000000000..0c63f1613 --- /dev/null +++ b/.github/workflows/release-tag.yml @@ -0,0 +1,26 @@ +on: + push: + tags: + - "v*" # Push events to matching v*, i.e. v1.0, v20.15.10 + +env: + REPO_URL: https://github.com/DivanteLtd/shopware-pwa + +name: Create Release + +jobs: + build: + name: Create Release + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@master + - name: Create Release for Tag + id: release_tag + uses: yyx990803/release-tag@master + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + body: | + Please refer to [CHANGELOG.md](${{ env.REPO_URL }}/blob/master/CHANGELOG.md) for details. diff --git a/jest.config.js b/jest.config.js index a5f706b0d..684c3e98a 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,4 +1,3 @@ -const lernaJson = require("./lerna.json"); const e2e = require("minimist")(process.argv.slice(2))["e2e"]; const isUnitTest = e2e !== "true"; @@ -6,7 +5,7 @@ const isUnitTest = e2e !== "true"; console.log(`Starting ${isUnitTest ? "unit" : "e2e"} tests...`); const UNIT_TEST_PATTERNS = [ - "/packages/**/__tests__/**/*spec.[jt]s?(x)" + "/packages/**/__tests__/**/*spec.[jt]s?(x)", ]; const E2E_TEST_PATTERNS = ["/packages/**/__e2e__/**/*spec.[jt]s?(x)"]; module.exports = { @@ -14,14 +13,14 @@ module.exports = { globals: { __DEV__: true, __TEST__: true, - __VERSION__: require("./packages/nuxt-module/package.json").version, + __VERSION__: require("./package.json").version, __BROWSER__: false, __BUNDLER__: true, __RUNTIME_COMPILE__: true, __GLOBAL__: false, __NODE_JS__: true, __FEATURE_OPTIONS__: true, - __FEATURE_SUSPENSE__: true + __FEATURE_SUSPENSE__: true, }, collectCoverage: isUnitTest, coverageDirectory: "coverage", @@ -31,7 +30,7 @@ module.exports = { "!packages/*/src/**/*.d.ts", "!packages/default-template/**", "!packages/cli/**", - "!**/interfaces/**" + "!**/interfaces/**", ], watchPathIgnorePatterns: ["/node_modules/", "/dist/", "/.git/"], modulePathIgnorePatterns: [".yalc"], @@ -40,8 +39,8 @@ module.exports = { "^@shopware-pwa/commons/(.*?)$": "/packages/commons/$1", "^@shopware-pwa/(.*?)/src$": "/packages/$1/src", "^@shopware-pwa/(.*?)/src/(.*?)$": "/packages/$1/src/$2", - "^@shopware-pwa/(.*?)$": "/packages/$1/src" + "^@shopware-pwa/(.*?)$": "/packages/$1/src", }, rootDir: __dirname, - testMatch: isUnitTest ? UNIT_TEST_PATTERNS : E2E_TEST_PATTERNS + testMatch: isUnitTest ? UNIT_TEST_PATTERNS : E2E_TEST_PATTERNS, }; diff --git a/package.json b/package.json index a6b8859f7..7c6431686 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { "name": "shopware-pwa", + "version": "0.1.0-alpha.5", "private": true, "workspaces": { "packages": [ @@ -25,7 +26,9 @@ "test:cypress": "cypress run", "test:coverage": "yarn test --coverage", "docs:dev": "vuepress dev docs", - "docs:build": "vuepress build docs" + "docs:build": "vuepress build docs", + "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s", + "release": "node scripts/release.js" }, "husky": { "hooks": { @@ -60,6 +63,7 @@ "conventional-changelog-cli": "^2.0.31", "coveralls": "^3.0.13", "cypress": "^4.4.1", + "enquirer": "^2.3.5", "execa": "^4.0.0", "faker": "^4.1.0", "fs-extra": "^9.0.0", @@ -75,6 +79,7 @@ "rollup-plugin-peer-deps-external": "^2.2.2", "rollup-plugin-terser": "^5.3.0", "rollup-plugin-typescript2": "^0.27.0", + "semver": "^7.3.2", "ts-jest": "^25.4.0", "tslib": "^1.11.1", "typedoc": "^0.17.4", diff --git a/packages/cli/package.json b/packages/cli/package.json index 5b34d6d0a..de958eff2 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -13,13 +13,11 @@ "compile": "tsc -p .", "copy-templates": "if [ -e ./src/templates ]; then cp -a ./src/templates ./build/; fi", "build": "yarn format && yarn lint && yarn clean-build && yarn compile && yarn copy-templates", - "prepublishOnly": "yarn build", "test": "jest", "watch": "jest --watch", "snapupdate": "jest --updateSnapshot", "coverage": "jest --coverage", - "snyk-protect": "snyk protect", - "prepublish": "npm run snyk-protect" + "snyk-protect": "snyk protect" }, "files": [ "tsconfig.json", @@ -32,7 +30,7 @@ ], "license": "MIT", "dependencies": { - "gluegun": "^4.2.0", + "gluegun": "^4.3.1", "lodash": "^4.17.15", "request": "^2.88.2", "snyk": "^1.316.1", diff --git a/packages/cli/src/types.ts b/packages/cli/src/types.ts index 07a21bbf2..540f4dd29 100644 --- a/packages/cli/src/types.ts +++ b/packages/cli/src/types.ts @@ -1 +1,5 @@ // export types + +export interface ShopwarePwaToolbox { + isProduction: boolean; +} diff --git a/packages/default-theme/package.json b/packages/default-theme/package.json index 9863f0461..e8251c1e1 100644 --- a/packages/default-theme/package.json +++ b/packages/default-theme/package.json @@ -6,8 +6,7 @@ "scripts": { "lint": "prettier --write './**/*.{js,vue}'", "test": "jest", - "snyk-protect": "snyk protect", - "prepublish": "npm run snyk-protect" + "snyk-protect": "snyk protect" }, "husky": { "hooks": { diff --git a/packages/nuxt-module/package.json b/packages/nuxt-module/package.json index fa96c50b3..492f366ee 100644 --- a/packages/nuxt-module/package.json +++ b/packages/nuxt-module/package.json @@ -35,8 +35,7 @@ "access": "public" }, "scripts": { - "snyk-protect": "snyk protect", - "prepublish": "npm run snyk-protect" + "snyk-protect": "snyk protect" }, "snyk": true } diff --git a/scripts/bootstrap.js b/scripts/bootstrap.js index c9f226228..8a8d9749c 100644 --- a/scripts/bootstrap.js +++ b/scripts/bootstrap.js @@ -3,12 +3,12 @@ const args = require("minimist")(process.argv.slice(2)); const fs = require("fs"); const path = require("path"); -const baseVersion = require("../lerna.json").version; +const baseVersion = require("../package.json").version; const packagesDir = path.resolve(__dirname, "../packages"); const files = fs.readdirSync(packagesDir); -files.forEach(shortName => { +files.forEach((shortName) => { if (!fs.statSync(path.join(packagesDir, shortName)).isDirectory()) { return; } @@ -36,9 +36,9 @@ files.forEach(shortName => { sideEffects: false, repository: { type: "git", - url: "git+https://github.com/DivanteLtd/shopware-pwa" + url: "git+https://github.com/DivanteLtd/shopware-pwa", }, - license: "MIT" + license: "MIT", }; fs.writeFileSync(pkgPath, JSON.stringify(json, null, 2)); } diff --git a/scripts/build.js b/scripts/build.js index 095807f67..8d5de9d76 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -93,6 +93,12 @@ async function build(target) { return; } + // run custom package build if there is one + if (pkg.scripts && pkg.scripts.build) { + await execa("yarn", ["build"], { stdio: "inherit", cwd: pkgDir }); + return; + } + // if building a specific format, do not remove dist. if (!formats) { await fs.remove(`${pkgDir}/dist`); diff --git a/scripts/release.js b/scripts/release.js new file mode 100644 index 000000000..f10cbc141 --- /dev/null +++ b/scripts/release.js @@ -0,0 +1,253 @@ +const args = require("minimist")(process.argv.slice(2)); +const fs = require("fs"); +const path = require("path"); +const chalk = require("chalk"); +const semver = require("semver"); +const currentVersion = require("../package.json").version; +const { prompt } = require("enquirer"); +const execa = require("execa"); + +const isCanaryRelease = args.canary; +const preId = args.preid || semver.prerelease(currentVersion)[0] || "alpha"; +const isDryRun = args.dry; +const skipTests = args.skipTests; +const skipBuild = args.skipBuild; +const packages = fs + .readdirSync(path.resolve(__dirname, "../packages")) + .filter((p) => !p.endsWith(".ts") && !p.startsWith(".")); + +const skippedPackages = []; + +const versionIncrements = [ + "patch", + "minor", + "major", + "prepatch", + "preminor", + "premajor", + "prerelease", +]; + +const inc = (i, prereleaseId = preId) => + semver.inc(currentVersion, i, prereleaseId); +const bin = (name) => path.resolve(__dirname, "../node_modules/.bin/" + name); +const run = (bin, args, opts = {}) => + execa(bin, args, { stdio: "inherit", ...opts }); +const dryRun = (bin, args, opts = {}) => + console.log(chalk.blue(`[dryrun] ${bin} ${args.join(" ")}`), opts); +const runIfNotDry = isDryRun ? dryRun : run; +const getPkgRoot = (pkg) => path.resolve(__dirname, "../packages/" + pkg); +const step = (msg) => console.log(chalk.cyan(msg)); + +async function main() { + let targetVersion = args._[0]; + step("\nReleasing from current version " + currentVersion); + if (isCanaryRelease) { + targetVersion = await calculateCanaryTargetVersion(); + step("CANARY RELEASE --> " + targetVersion); + } + + if (!targetVersion) { + // no explicit version, offer suggestions + const { release } = await prompt({ + type: "select", + name: "release", + message: "Select release type", + choices: versionIncrements + .map((i) => `${i} (${inc(i)})`) + .concat(["custom"]), + }); + + if (release === "custom") { + targetVersion = ( + await prompt({ + type: "input", + name: "version", + message: "Input custom version", + initial: currentVersion, + }) + ).version; + } else { + targetVersion = release.match(/\((.*)\)/)[1]; + } + } + + if (!semver.valid(targetVersion)) { + throw new Error(`invalid target version: ${targetVersion}`); + } + + if (!isCanaryRelease) { + const { yes } = await prompt({ + type: "confirm", + name: "yes", + message: `Releasing v${targetVersion}. Confirm?`, + }); + + if (!yes) { + return; + } + } + + // run tests before release + step("\nRunning tests..."); + if (!skipTests && !isDryRun) { + await run(bin("jest"), ["--clearCache"]); + await run("yarn", ["test", "--runInBand"]); + } else { + console.log(`(skipped)`); + } + + // update all package versions and inter-dependencies + step("\nUpdating cross dependencies..."); + updateVersions(targetVersion); + + // build all packages with types + step("\nBuilding all packages..."); + if (!skipBuild && !isDryRun) { + await run("yarn", ["build", "--release"]); + // test generated dts files + // step("\nVerifying type declarations..."); + // await run(bin("tsd")); + } else { + console.log(`(skipped)`); + } + + // generate changelog + step("\nGnerating changelog..."); + if (!isCanaryRelease) { + await run(`yarn`, ["changelog"]); + } else { + console.log(`(skipped)`); + } + + if (!isCanaryRelease) { + const { stdout } = await run("git", ["diff"], { stdio: "pipe" }); + if (stdout) { + step("\nCommitting changes..."); + await runIfNotDry("git", ["add", "-A"]); + await runIfNotDry("git", ["commit", "-m", `release: v${targetVersion}`]); + } else { + console.log("No changes to commit."); + } + } + + // publish packages + step("\nPublishing packages..."); + for (const pkg of packages) { + await publishPackage(pkg, targetVersion, runIfNotDry); + } + + // push to GitHub + if (!isCanaryRelease) { + step("\nPushing to GitHub..."); + await runIfNotDry("git", ["tag", `v${targetVersion}`]); + await runIfNotDry("git", ["push", "origin", `refs/tags/v${targetVersion}`]); + await runIfNotDry("git", ["push"]); + } + + if (isDryRun) { + console.log(`\nDry run finished - run git diff to see package changes.`); + } + + if (skippedPackages.length) { + console.log( + chalk.yellow( + `The following packages are skipped and NOT published:\n- ${skippedPackages.join( + "\n- " + )}` + ) + ); + } + console.log(); +} + +async function calculateCanaryTargetVersion() { + if (!isCanaryRelease) return null; + const { stdout: commitsCount } = await execa("git", [ + "rev-list", + "--all", + "--count", + ]); + const incrementedVrsion = inc("prerelease", "canary"); + const v = incrementedVrsion.split("canary."); + v[v.length - 1] = commitsCount; + return v.join("canary."); +} + +function updateVersions(version) { + // 1. update root package.json + updatePackage(path.resolve(__dirname, ".."), version); + // 2. update all packages + packages.forEach((p) => updatePackage(getPkgRoot(p), version)); +} + +function updatePackage(pkgRoot, version) { + const pkgPath = path.resolve(pkgRoot, "package.json"); + const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")); + pkg.version = version; + updateDeps(pkg, "dependencies", version); + updateDeps(pkg, "peerDependencies", version); + fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n"); +} + +function updateDeps(pkg, depType, version) { + const deps = pkg[depType]; + if (!deps) return; + Object.keys(deps).forEach((dep) => { + if ( + dep.startsWith("@shopware-pwa") && + packages.includes(dep.replace(/^@shopware-pwa\//, "")) + ) { + console.log( + chalk.yellow(`${pkg.name} -> ${depType} -> ${dep}@${version}`) + ); + deps[dep] = version; + } + }); +} + +async function publishPackage(pkgName, version, runIfNotDry) { + if (skippedPackages.includes(pkgName)) { + return; + } + const pkgRoot = getPkgRoot(pkgName); + const pkgPath = path.resolve(pkgRoot, "package.json"); + const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")); + if (pkg.private) { + return; + } + + const releaseTag = isCanaryRelease ? "canary" : null; + // version could inherit fom package prerelease as well + // const releaseTag = semver.prerelease(version)[0] || null + + step(`Publishing ${pkgName}...`); + try { + await runIfNotDry( + "yarn", + [ + "publish", + "--new-version", + version, + ...(releaseTag ? ["--tag", releaseTag] : []), + "--access", + "public", + ], + { + cwd: pkgRoot, + stdio: "pipe", + } + ); + console.log(chalk.green(`Successfully published ${pkgName}@${version}`)); + } catch (e) { + if (e.stderr.match(/previously published/)) { + console.log(chalk.red(`Skipping already published: ${pkgName}`)); + } else { + throw e; + } + } +} + +main().catch((err) => { + console.error(err); +}); diff --git a/scripts/utils.js b/scripts/utils.js index e72acdf28..239aee2ec 100644 --- a/scripts/utils.js +++ b/scripts/utils.js @@ -1,13 +1,13 @@ const fs = require("fs"); const chalk = require("chalk"); -const ownBuildProcessPackages = ["cli", "default-theme", "commons"]; +const ownBuildProcessPackages = ["default-theme", "commons"]; const allTargets = (exports.allTargets = fs .readdirSync("packages") - .filter(f => !!fs.statSync(`packages/${f}`).isDirectory())); + .filter((f) => !!fs.statSync(`packages/${f}`).isDirectory())); -const targets = (exports.targets = allTargets.filter(f => { +const targets = (exports.targets = allTargets.filter((f) => { if ( !fs.statSync(`packages/${f}`).isDirectory() || ownBuildProcessPackages.includes(f) @@ -23,7 +23,7 @@ const targets = (exports.targets = allTargets.filter(f => { exports.fuzzyMatchTarget = (partialTargets, includeAllMatching) => { const matched = []; - partialTargets.forEach(partialTarget => { + partialTargets.forEach((partialTarget) => { for (const target of targets) { if (target.match(partialTarget)) { matched.push(target); diff --git a/yarn.lock b/yarn.lock index b556289bf..7f22024bd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7603,10 +7603,17 @@ enhanced-resolve@^4.0.0, enhanced-resolve@^4.1.0, enhanced-resolve@^4.1.1: memory-fs "^0.5.0" tapable "^1.0.0" -enquirer@2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.2.tgz#1c30284907cadff5ed2404bd8396036dd3da070e" - integrity sha512-PLhTMPUXlnaIv9D3Cq3/Zr1xb7soeDDgunobyCmYLUG19n24dvC8i+ZZgm2DekGpDnx7JvFSHV7lxfM58PMtbA== +enquirer@2.3.4: + version "2.3.4" + resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.4.tgz#c608f2e1134c7f68c1c9ee056de13f9b31076de9" + integrity sha512-pkYrrDZumL2VS6VBGDhqbajCM2xpkUNLuKfGPjfKaSIBKYopQbqEFyrOkRMIb2HDR/rO1kGhEt/5twBwtzKBXw== + dependencies: + ansi-colors "^3.2.1" + +enquirer@^2.3.5: + version "2.3.5" + resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.5.tgz#3ab2b838df0a9d8ab9e7dff235b0e8712ef92381" + integrity sha512-BNT1C08P9XD0vNg3J475yIUG+mVdp9T6towYFHUv897X0KoHBjB1shyrNmhmtHWKP17iSWgo7Gqh7BBuzLZMSA== dependencies: ansi-colors "^3.2.1" @@ -9115,10 +9122,10 @@ globule@^1.0.0: lodash "~4.17.12" minimatch "~3.0.2" -gluegun@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/gluegun/-/gluegun-4.2.0.tgz#bd322710611463c9ebea8a6f14a851983110e8d0" - integrity sha512-ABzaq9A5Gy1BzchyxkJKuX+kSRjpaorj9DMVH+drW+2LRmLanNxRUdzCcWTX9qiV0sbp1AaOnOtv5uhREeyifA== +gluegun@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/gluegun/-/gluegun-4.3.1.tgz#b93a8619e2e9546ab3451ad94f7be9097c75083a" + integrity sha512-TyOf60807SUYvd0v7fiRbhWrBSl9974clwZwCVc3qxYB83TQQQUSggwcz3yUeOl1MBALpwrz0Q0GUmi25quVOg== dependencies: apisauce "^1.0.1" app-module-path "^2.2.0" @@ -9127,7 +9134,7 @@ gluegun@^4.2.0: cosmiconfig "6.0.0" cross-spawn "^7.0.0" ejs "^2.6.1" - enquirer "2.3.2" + enquirer "2.3.4" execa "^3.0.0" fs-jetpack "^2.2.2" lodash.camelcase "^4.3.0"