Skip to content

Commit

Permalink
feat: support versioning and releasing with yarn plugins instead of `…
Browse files Browse the repository at this point in the history
…lerna` (#108)
  • Loading branch information
AlCalzone authored Jan 10, 2022
1 parent 238553f commit 636e22f
Show file tree
Hide file tree
Showing 15 changed files with 1,850 additions and 3,905 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test-and-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ jobs:
run: |
npm config set //registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}
npm whoami
yarn lerna publish from-package --yes
yarn workspaces foreach npm publish --tolerate-republish
- name: Create Github Release
uses: actions/create-release@v1
Expand Down
2 changes: 1 addition & 1 deletion .releaseconfig.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"plugins": ["lerna", "license"],
"plugins": ["license"],
"exec": {
"before_commit": "yarn run build"
}
Expand Down
8 changes: 8 additions & 0 deletions .yarn/plugins/@yarnpkg/plugin-changed.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/* eslint-disable */
module.exports = {
name: "@yarnpkg/plugin-changed",
factory: function (require) {
var plugin;(()=>{"use strict";var e={d:(t,o)=>{for(var n in o)e.o(o,n)&&!e.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:o[n]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t),r:e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}},t={};e.r(t),e.d(t,{default:()=>f});const o=require("@yarnpkg/cli"),n=require("clipanion"),r=require("@yarnpkg/core");function a(e){const{project:t}=e,o=new Set;return function e({manifest:n}){for(const a of r.Manifest.hardDependencies)for(const r of n.getForScope(a).values()){const n=t.tryWorkspaceByDescriptor(r);n&&!o.has(n)&&(o.add(n),e(n))}}(e),[...o]}function s(e){const t=new Set;for(const o of e.project.workspaces){a(o).some(t=>r.structUtils.areLocatorsEqual(t.locator,e.locator))&&t.add(o)}return[...t]}var i=function(e,t,o,n){var r,a=arguments.length,s=a<3?t:null===n?n=Object.getOwnPropertyDescriptor(t,o):n;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(e,t,o,n);else for(var i=e.length-1;i>=0;i--)(r=e[i])&&(s=(a<3?r(s):a>3?r(t,o,s):r(t,o))||s);return a>3&&s&&Object.defineProperty(t,o,s),s};class c extends o.BaseCommand{async listWorkspaces(e){const{stdout:t}=await r.execUtils.execvp("git",["diff","--name-only",...this.gitRange?[this.gitRange]:[]],{cwd:e.cwd,strict:!0}),o=function(e,t){const o=new Set;for(const n of e.workspaces){if(t.some(e=>e.startsWith(n.relativeCwd))&&!o.has(n)){o.add(n);for(const e of s(n))o.add(e)}}return[...o]}(e,t.split(/\r?\n/)),n=this.include||[],a=this.exclude||[];return o.filter(e=>{const t=r.structUtils.stringifyIdent(e.locator);if(t){if(n.length&&!n.includes(t))return!1;if(a.length&&a.includes(t))return!1}return!0})}}i([n.Command.String("--git-range")],c.prototype,"gitRange",void 0),i([n.Command.Array("--include")],c.prototype,"include",void 0),i([n.Command.Array("--exclude")],c.prototype,"exclude",void 0);var d=function(e,t,o,n){var r,a=arguments.length,s=a<3?t:null===n?n=Object.getOwnPropertyDescriptor(t,o):n;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(e,t,o,n);else for(var i=e.length-1;i>=0;i--)(r=e[i])&&(s=(a<3?r(s):a>3?r(t,o,s):r(t,o))||s);return a>3&&s&&Object.defineProperty(t,o,s),s};class l extends c{constructor(){super(...arguments),this.json=!1}async execute(){const e=await r.Configuration.find(this.context.cwd,this.context.plugins),{project:t,workspace:n}=await r.Project.find(e,this.context.cwd);if(!n)throw new o.WorkspaceRequiredError(t.cwd,this.context.cwd);return(await r.StreamReport.start({configuration:e,json:this.json,stdout:this.context.stdout},async e=>{const o=await this.listWorkspaces(t);for(const t of o)e.reportInfo(null,t.relativeCwd),e.reportJson({name:t.manifest.name?r.structUtils.stringifyIdent(t.manifest.name):null,location:t.relativeCwd})})).exitCode()}}l.usage=n.Command.Usage({description:"List changed workspaces and their dependents",details:"\n If the `--json` flag is set the output will follow a JSON-stream output also known as NDJSON (https://github.com/ndjson/ndjson-spec).\n ",examples:[["Find changed files within a Git range","yarn changed list --git-range 93a9ed8..4ef2c61"],["Include or exclude workspaces","yarn changed list --include @foo/a --exclude @foo/b"]]}),d([n.Command.Boolean("--json")],l.prototype,"json",void 0),d([n.Command.Path("changed","list")],l.prototype,"execute",null);var p=function(e,t,o,n){var r,a=arguments.length,s=a<3?t:null===n?n=Object.getOwnPropertyDescriptor(t,o):n;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(e,t,o,n);else for(var i=e.length-1;i>=0;i--)(r=e[i])&&(s=(a<3?r(s):a>3?r(t,o,s):r(t,o))||s);return a>3&&s&&Object.defineProperty(t,o,s),s};class u extends c{constructor(){super(...arguments),this.args=[],this.verbose=!1,this.parallel=!1,this.interlaced=!1,this.topological=!1}async execute(){const e=await r.Configuration.find(this.context.cwd,this.context.plugins),{project:t,workspace:n}=await r.Project.find(e,this.context.cwd);if(!n)throw new o.WorkspaceRequiredError(t.cwd,this.context.cwd);const a=await this.listWorkspaces(t);if(!a.length){return(await r.StreamReport.start({configuration:e,stdout:this.context.stdout},async e=>{e.reportInfo(null,"No workspaces changed")})).exitCode()}return this.cli.run(["workspaces","foreach",...a.reduce((e,t)=>[...e,"--include",r.structUtils.stringifyIdent(t.locator)],[]),...this.verbose?["--verbose"]:[],...this.parallel?["--parallel"]:[],...this.interlaced?["--interlaced"]:[],...this.topological?["--topological"]:[],...this.jobs?["--jobs",""+this.jobs]:[],this.commandName,...this.args],{cwd:t.cwd})}}u.usage=n.Command.Usage({description:"Run a command on changed workspaces and their dependents",details:"\n This command will run a given sub-command on changed workspaces and workspaces depends on them.\n\n Check the documentation for `yarn workspace foreach` for more details.\n ",examples:[["Run build scripts on changed workspaces","yarn changed foreach run build"],["Find changed files within a Git range","yarn changed foreach --git-range 93a9ed8..4ef2c61 run build"]]}),p([n.Command.String()],u.prototype,"commandName",void 0),p([n.Command.Proxy()],u.prototype,"args",void 0),p([n.Command.Boolean("-v,--verbose")],u.prototype,"verbose",void 0),p([n.Command.Boolean("-p,--parallel")],u.prototype,"parallel",void 0),p([n.Command.Boolean("-i,--interlaced")],u.prototype,"interlaced",void 0),p([n.Command.Boolean("-t,--topological")],u.prototype,"topological",void 0),p([n.Command.String("-j,--jobs")],u.prototype,"jobs",void 0),p([n.Command.Path("changed","foreach")],u.prototype,"execute",null);const f={commands:[l,u]};plugin=t})();
return plugin;
}
};
550 changes: 550 additions & 0 deletions .yarn/plugins/@yarnpkg/plugin-version.cjs

Large diffs are not rendered by default.

28 changes: 28 additions & 0 deletions .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs

Large diffs are not rendered by default.

768 changes: 768 additions & 0 deletions .yarn/releases/yarn-3.1.1.cjs

Large diffs are not rendered by default.

632 changes: 0 additions & 632 deletions .yarn/releases/yarn-sources.cjs

This file was deleted.

8 changes: 7 additions & 1 deletion .yarnrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,11 @@ nodeLinker: node-modules
plugins:
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: "@yarnpkg/plugin-interactive-tools"
- path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
spec: "@yarnpkg/plugin-workspace-tools"
- path: .yarn/plugins/@yarnpkg/plugin-version.cjs
spec: "@yarnpkg/plugin-version"
- path: .yarn/plugins/@yarnpkg/plugin-changed.cjs
spec: "https://github.com/Dcard/yarn-plugins/releases/latest/download/plugin-changed.js"

yarnPath: .yarn/releases/yarn-sources.cjs
yarnPath: .yarn/releases/yarn-3.1.1.cjs
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
Placeholder for the next version (at the beginning of the line):
## **WORK IN PROGRESS**
-->
## **WORK IN PROGRESS**
* Support managing Yarn monorepos without `lerna`

## 3.4.3 (2022-01-10)
* Replace `colors` dependency with `picocolors`

Expand Down
42 changes: 41 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Automate the monotonous tasks when it comes to releasing new versions of your pr
- Update the changelog headline with the new version and release date whenever a release is made
- Move old changelog entries into another file
- Add the changelog to the release commit and create a tag for it.
- Support for monorepos managed with `lerna`
- Support for monorepos managed with `lerna` or `yarn workspaces` (using additional `yarn` plugins)
- Support for custom scripts during the release lifecycle
- Check licenses for outdated copyrights
- Dry runs and checking for errors
Expand Down Expand Up @@ -211,6 +211,37 @@ Example:
npm run release minor -- --yes
```
## Monorepo support
You can version monorepos in two different ways.
### Using the `yarn` package manager
_**Note:** If possible, prefer this way. Lerna does not support some advanced Yarn features, like `workspace:*` dependency ranges._
Starting with `yarn v3.1.0`, the release script can use Yarn plugins to manage versions and releases of monorepos. These plugins can be installed in your repository as follows:
```bash
yarn plugin import workspace-tools
yarn plugin import version
yarn plugin import https://github.com/Dcard/yarn-plugins/releases/latest/download/plugin-changed.js
```
You also need to make sure that there is a `"version"` field in the root `package.json` file.
To release changed packages, simply run the following command after bumping the versions (ideally during your CI build):
```bash
# without dist-tags
yarn workspaces foreach npm publish --tolerate-republish
# with dist-tags
yarn workspaces foreach npm publish --tolerate-republish --tag my-tag
```
### Using `lerna`
Set up [`lerna`](https://github.com/lerna/lerna) in fixed versioning mode and install and enable the `lerna` plugin (see below). Note that this disables the use of `yarn` plugins.
## Plugins
Since version 3, the release script is separated into several plugins, making it easy to extend it with custom checks. The following plugins are installed and loaded by default and don't need to be installed separately:
Expand Down Expand Up @@ -498,6 +529,15 @@ jobs:
npm config set //registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}
npm whoami
npm publish
# To publish multiple packages from monorepos at once, you need to Replace
# the previous command with one of the following:
#
# * When using lerna (requires lerna to be installed globally):
# lerna publish from-package --yes
# * When using lerna with yarn (doesn't require a global install):
# yarn lerna publish from-package --yes
# * When using yarn v3.1+ (lerna not required)
# yarn workspaces foreach npm publish --tolerate-republish
- name: Create Github Release
uses: actions/create-release@v1
Expand Down
24 changes: 0 additions & 24 deletions lerna.json

This file was deleted.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"name": "@alcalzone/release-script-repo",
"version": "3.4.3",
"description": "Release script to automatically increment version numbers and push git tags",
"keywords": [
"release",
Expand Down Expand Up @@ -36,7 +37,6 @@
"eslint-plugin-prettier": "^4.0.0",
"husky": "^7.0.2",
"jest": "^27.1.1",
"lerna": "^4.0.0",
"prettier": "^2.4.0",
"source-map-support": "^0.5.20",
"ts-node": "^10.2.1",
Expand Down Expand Up @@ -70,5 +70,5 @@
"path": "./node_modules/cz-conventional-changelog"
}
},
"packageManager": "yarn@3.0.2"
"packageManager": "yarn@3.1.1"
}
205 changes: 204 additions & 1 deletion packages/plugin-package/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,28 @@ import PackagePlugin from ".";

jest.mock("@alcalzone/pak");

const fixtures = {
yarnrc_commented_out: `
plugins:
- path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
spec: "@yarnpkg/plugin-workspace-tools"
- path: .yarn/plugins/@yarnpkg/plugin-changed.cjs
spec: "https://github.com/Dcard/yarn-plugins/releases/latest/download/plugin-changed.js"
# commented out, but required:
# - path: .yarn/plugins/@yarnpkg/plugin-version.cjs
# spec: "@yarnpkg/plugin-version"
`.trim(),
yarnrc_complete: `
plugins:
- path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
spec: "@yarnpkg/plugin-workspace-tools"
- path: .yarn/plugins/@yarnpkg/plugin-changed.cjs
spec: "https://github.com/Dcard/yarn-plugins/releases/latest/download/plugin-changed.js"
- path: .yarn/plugins/@yarnpkg/plugin-version.cjs
spec: "@yarnpkg/plugin-version"
`.trim(),
};

describe("Package plugin", () => {
describe("check stage", () => {
let testFS: TestFS;
Expand Down Expand Up @@ -71,7 +93,7 @@ describe("Package plugin", () => {
});
});

it(`"errors when package scripts are outdated`, async () => {
it(`errors when package scripts are outdated`, async () => {
const pkgPlugin = new PackagePlugin();
const context = createMockContext({
plugins: [pkgPlugin],
Expand Down Expand Up @@ -100,6 +122,100 @@ describe("Package plugin", () => {
});
});

describe("check stage (monorepo)", () => {
let testFS: TestFS;
let testFSRoot: string;
beforeEach(async () => {
testFS = new TestFS();
testFSRoot = await testFS.getRoot();
});
afterEach(async () => {
await testFS.remove();
});

it("raises a fatal error when neither lerna nor yarn plugins are available", async () => {
const pkgPlugin = new PackagePlugin();
const context = createMockContext({
plugins: [pkgPlugin],
cwd: testFSRoot,
});

await testFS.create({
"package.json": JSON.stringify({
name: "test-package",
version: "1.2.3",
workspaces: ["packages/*"],
}),
});

await assertReleaseError(() => pkgPlugin.executeStage(context, DefaultStages.check), {
fatal: true,
messageMatches: /monorepo/i,
});
});

it("raises a fatal error when some yarn plugins are unavailable", async () => {
const pkgPlugin = new PackagePlugin();
const context = createMockContext({
plugins: [pkgPlugin],
cwd: testFSRoot,
});

await testFS.create({
"package.json": JSON.stringify({
name: "test-package",
version: "1.2.3",
workspaces: ["packages/*"],
}),
".yarnrc.yml": fixtures.yarnrc_commented_out,
});

await assertReleaseError(() => pkgPlugin.executeStage(context, DefaultStages.check), {
fatal: true,
messageMatches: /plugin import version/i,
});
});

it("does not error when the monorepo doesn't have any sub-packages", async () => {
const pkgPlugin = new PackagePlugin();
const context = createMockContext({
plugins: [pkgPlugin],
cwd: testFSRoot,
});

await testFS.create({
"package.json": JSON.stringify({
name: "test-package",
version: "1.2.3",
workspaces: [],
}),
});

await pkgPlugin.executeStage(context, DefaultStages.check);
expect(context.errors).toHaveLength(0);
});

it("does not error when lerna is available", async () => {
const pkgPlugin = new PackagePlugin();
const context = createMockContext({
plugins: [pkgPlugin],
cwd: testFSRoot,
});
context.setData("lerna", true);

await testFS.create({
"package.json": JSON.stringify({
name: "test-package",
version: "1.2.3",
workspaces: ["packages/*"],
}),
});

await pkgPlugin.executeStage(context, DefaultStages.check);
expect(context.errors).toHaveLength(0);
});
});

describe("edit stage", () => {
let testFS: TestFS;
let testFSRoot: string;
Expand Down Expand Up @@ -207,6 +323,59 @@ describe("Package plugin", () => {
const fileContent = await fs.readJson(packPath);
expect(fileContent).toEqual(pack);
});

it("defers the versioning to yarn for yarn-managed monorepos (no lerna)", async () => {
const pkgPlugin = new PackagePlugin();
const context = createMockContext({
plugins: [pkgPlugin],
cwd: testFSRoot,
});

const pack = {
name: "test-package",
version: "1.2.3",
workspaces: ["packages/*"],
};
await testFS.create({
"package.json": JSON.stringify(pack, null, 2),
".yarnrc.yml": fixtures.yarnrc_complete,
});

const newVersion = "1.2.4";
context.setData("package.json", pack);
context.setData("version_new", newVersion);
context.setData("monorepo", "yarn");

context.sys.mockExec((cmd) =>
cmd.includes("changed list")
? `
{"name":"@package/foo","location":"packages/foo"}
{"name":"@package/bar","location":"packages/bar"}`
: "",
);

await pkgPlugin.executeStage(context, DefaultStages.edit);
expect(context.errors).toHaveLength(0);

expect(context.cli.log).toHaveBeenCalledWith(expect.stringMatching("@package/foo"));
expect(context.cli.log).toHaveBeenCalledWith(expect.stringMatching("@package/bar"));

const expectedCommands = [
[
"yarn",
"changed",
"foreach",
`--git-range=v${pack.version}`,
"version",
newVersion,
"--deferred",
],
["yarn", "version", "apply", "--all"],
];
for (const [cmd, ...args] of expectedCommands) {
expect(context.sys.exec).toHaveBeenCalledWith(cmd, args, expect.anything());
}
});
});

describe("commit stage", () => {
Expand Down Expand Up @@ -304,5 +473,39 @@ describe("Package plugin", () => {
expect.stringMatching("Updating lockfile failed: NOOOO"),
);
});

it("does nothing for yarn-managed monorepos (no lerna)", async () => {
const pkgPlugin = new PackagePlugin();
const context = createMockContext({
plugins: [pkgPlugin],
cwd: testFSRoot,
});
// Don't throw when calling system commands
context.sys.mockExec(() => "");

const packPath = path.join(testFSRoot, "package.json");
const pack = {
name: "test-package",
version: "1.2.3",
workspaces: ["packages/*"],
};

await testFS.create({
"package.json": JSON.stringify(pack, null, 2),
".yarnrc.yml": fixtures.yarnrc_complete,
});

context.setData("package.json", pack);
context.setData("version_new", "1.2.4");
context.setData("monorepo", "yarn");

await pkgPlugin.executeStage(context, DefaultStages.commit);
expect(context.errors).toHaveLength(0);

const fileContent = await fs.readJson(packPath);
expect(fileContent).toEqual(pack);

expect(context.sys.execRaw).not.toHaveBeenCalled();
});
});
});
Loading

0 comments on commit 636e22f

Please sign in to comment.