Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add flow for monorepo w/ independent versions #6

Merged
merged 42 commits into from
Aug 2, 2022
Merged

Conversation

mcmire
Copy link
Contributor

@mcmire mcmire commented Jul 18, 2022

This commit introduces a rough cut of a flow which supports a monorepo
with an independent versioning strategy. This flow:

  • Generates a "release specification" file containing all of the
    workspace packages within the repo (this will be changed to all
    updated packages in a future commit)
  • Opens the user's editor (if one can be detected) and waits for them to
    edit it
  • Parses the edited version of the release spec
  • Applies the release spec by:
    • Updating the version of the root package with the current date
      (note: the format of the version string will be changed in a future
      commit)
    • Updating the versions of all of the packages listed in the release
      spec
    • Adds new sections to the changelogs of all of the packages listed in
      the release spec

v8 isn't quite reliable when using Node 14 for development (it has too
many false negatives). We can consider changing this back if/when we
switch to Node 16 or later, but for now we will stick to Babel.
This commit introduces a rough cut of a flow which supports a monorepo
with an independent versioning strategy. This flow:

* Generates a "release specification" file containing all of the
  workspace packages within the repo (this will be changed to all
  _updated_ packages in a future commit)
* Opens the user's editor (if one can is detected) and waits for them to
  edit it
* Parses the edited version of the release spec
* Applies the release spec by:
  * Updating the version of the root package with the current date
    (note: the format of the version string will be changed in a future
    commit)
  * Updating the versions of all of the packages listed in the release
    spec
  * Adds new sections to the changelogs of all of the packages listed in
    the release spec
@mcmire mcmire requested a review from a team as a code owner July 18, 2022 22:48
Copy link
Contributor Author

@mcmire mcmire left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, here's the PR I've been working on for the past couple of weeks. I know this is a giant PR 😓 . I really tried to pare this branch down as much as I could, but in addition to introducing a flow this also adds a bunch of infrastructure as well which would not be used without that flow.

I'm happy to walk through this code on a call, but if you want to brave a readthrough, I would recommend skipping tests and reading the files in this order (all of these files are in src/):

  • index.ts
  • main.ts
  • initialization-utils.ts
  • inputs-utils.ts
  • project-utils.ts
  • git-utils.ts
  • package-utils.ts
  • package-manifest-utils.ts
  • monorepo-workflow-utils.ts
  • editor-utils.ts
  • release-specification-utils.ts
  • workflow-utils.ts

Also, there are a lot of TODOs in the code. I plan on extracting those into another PR as I want to be able to link to places in the code. Done.

jest.config.js Outdated
coveragePathIgnorePatterns: [
'/node_modules/',
'/src/index.ts',
'/src/inputs-utils.ts',
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file uses yargs to parse command-line arguments. It's pretty hard to mock yargs given its flexible API, and this is the only thing this file does, so I opted not to write tests for it. If there's a way to do this however then I'd love to know!

src/editor-utils.ts Outdated Show resolved Hide resolved
src/git-utils.ts Outdated
* @returns The HTTPS URL of the repository, e.g.
* `https://github.com/OrganizationName/RepositoryName`.
*/
export async function getRepositoryHttpsUrl(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of these functions were lifted directly from action-create-release-pr, such as this one.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh, we could use this function in @metamask/auto-changelog. Right now that only supports HTTPS repository URLs.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you talking about replacing this function. If so, that pulls the URL from package.json, whereas this one pulls the URL from Git, so that's also a difference.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, good point, I was just thinking of the second half of this function that performs normalization.

src/monorepo-workflow-utils.test.ts Outdated Show resolved Hide resolved
@@ -0,0 +1,348 @@
import path from 'path';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A lot of this file was stolen from package-utils.ts in action-utils and consolidated/simplified.

@@ -0,0 +1,298 @@
import fs, { WriteStream } from 'fs';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is all new stuff.

@@ -0,0 +1,74 @@
import { Package } from './package-utils';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is new stuff.

* @param args.stderr - A stream that can be used to write to standard error.
* @returns The result of writing to the changelog.
*/
async function updatePackageChangelog({
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updatePackageChangelog and updatePackage were more or less stolen from package-operations in action-create-release-pr.

@@ -0,0 +1,244 @@
import type { WriteStream } from 'fs';
Copy link
Contributor Author

@mcmire mcmire Jul 18, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is new stuff too.

src/file-utils.ts Outdated Show resolved Hide resolved
@mcmire
Copy link
Contributor Author

mcmire commented Jul 18, 2022

Also I know that there are linting errors, I will address those tomorrow 😓

jest.config.js Outdated Show resolved Hide resolved
jest.config.js Outdated Show resolved Hide resolved
tsconfig.json Outdated Show resolved Hide resolved
Base automatically changed from use-babel-for-coverage to main July 19, 2022 16:25
.eslintrc.js Outdated Show resolved Hide resolved
validation: validationForManifestDependenciesField,
defaultValue: {},
});
return { ...obj, [fieldName]: dependencies };
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not include fields that are not in the original manifest. This means that when we write back to the manifest in updatePackage we are adding unnecessary data. I'll make a note to fix this in a future PR.

[ManifestFieldNames.Version]: version,
[ManifestFieldNames.Workspaces]: workspaces,
[ManifestFieldNames.Private]: privateValue,
...dependencyFields,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function looks for fields in the manifest that we care about, but completely discards all fields that we don't care about. This isn't right — this means when we write back to the manifest in updatePackage we are removing critical fields. I've made a note to fix this in a future PR.

jest.config.js Outdated Show resolved Hide resolved
@mcmire mcmire self-assigned this Jul 25, 2022
Copy link
Member

@Gudahtt Gudahtt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like how this is organized! I will try to finish reading through the non-test code tomorrow morning, and hopefully will start on the tests as well.

src/index.ts Outdated Show resolved Hide resolved
src/index.ts Outdated Show resolved Hide resolved
src/index.ts Outdated Show resolved Hide resolved
src/initialization-utils.ts Outdated Show resolved Hide resolved
src/misc-utils.ts Outdated Show resolved Hide resolved
src/inputs-utils.ts Outdated Show resolved Hide resolved
src/project-utils.test.ts Outdated Show resolved Hide resolved
src/project-utils.test.ts Outdated Show resolved Hide resolved
src/git-utils.test.ts Outdated Show resolved Hide resolved
* returns that `message` by default.
* @returns A new error object.
*/
export function wrapError(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you seen this proposal before? https://github.com/tc39/proposal-error-cause
It's now stable, and was shipped in Node.js v16.9 and in all major browsers, and has a shim. It's a standard way of "wrapping" an error, and I think it would be better to use this pattern than by decomposing and reconstructing an error like is done here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't! This is cool, but does Node v14 (or v16.9) use the cause property when printing out an error? In other words, if cause points to another Error object that has a stack, I'd want that error to get automatically printed along with the new error — kind of like this, but without having to use an explicit function to get that stack. I think that'd be my only concern.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like that's how it looks in practice:

$ node
Welcome to Node.js v16.14.2.
Type ".help" for more information.
> const innerError = new Error('inner')
undefined
> const outerError = new Error('outer', { cause: innerError })
undefined
> console.error(outerError)
Error: outer
    at REPL2:1:20
    at Script.runInThisContext (node:vm:129:12)
    ... 7 lines matching cause stack trace ...
    at REPLServer.Interface._line (node:readline:864:8) {
  [cause]: Error: inner
      at REPL1:1:20
      at Script.runInThisContext (node:vm:129:12)
      at REPLServer.defaultEval (node:repl:566:29)
      at bound (node:domain:421:15)
      at REPLServer.runBound [as eval] (node:domain:432:12)
      at REPLServer.onLine (node:repl:893:10)
      at REPLServer.emit (node:events:538:35)
      at REPLServer.emit (node:domain:475:12)
      at REPLServer.Interface._onLine (node:readline:487:10)
      at REPLServer.Interface._line (node:readline:864:8)
}
undefined
> 

Copy link
Contributor Author

@mcmire mcmire Jul 27, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... that would work for Node v16, but I'm not sure it would work for v14, even when using a polyfill, because v8 doesn't know to look for cause when producing a stack trace. This would particularly impact our fs.promises wrappers because it means that when an error is output, the stack we want to add in the first place would get lost. But this would also impact every place we're using wrapError in order to simply prefix existing error messages, because the original error message would get lost too. So I'm not sure that this is something that would help us until we switched everything to v16.

Thoughts?

Copy link
Member

@Gudahtt Gudahtt Jul 27, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would particularly impact our fs.promises wrappers because it means that when an error is output, the stack we want to add in the first place would get lost.

Hmm, would it? I thought the stack that we cared about was the outer error stack, because the inner one had no stack.

But yes that's a fair point overall, it looks like this would only show the outer error on Node.js v14. We could account for that by incorporating the inner error message in the outer message. Or by using pony-cause utilities instead of just console.error(error) directly. Or accepting that using this in v14 would result in degraded error messages (we can set .npmrc to v16.x in all of our libraries and still keep the minimum set to v14).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure about there being a real need to recursively search through the cause stack to find a code.

Is there a case where we don't know where to find the code we're looking for? Like in this case, even if we keep the code on the wrapped error, we know it will be on error.cause.code. If we don't know where the error with the code is, why would we check for it? Checking recursively for any cause risks mistaking one error for another.

Another thing to consider here is that we can make these fs-wrapper errors be whatever we like. We could forward the code along to the outer error, or we could use error classes to distinguish cases.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a case where we don't know where to find the code we're looking for? Like in this case, even if we keep the code on the wrapped error, we know it will be on error.cause.code.

Sure, take this example in package.test.ts, where we're reading the changelog file. You and I know what readFile is doing, but I don't want anyone else to have to know that. They should just be able to grab the code from the error object as if it's not wrapped.

Since that's the goal, and since I don't want to add too much magic to our fs wrappers here so they can be used in a flexible way, I'll just copy code over.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, see 761377f. I opted to use pony-cause instead of error-cause as I didn't want to change any globals.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You and I know what readFile is doing, but I don't want anyone else to have to know that. They should just be able to grab the code from the error object as if it's not wrapped.

I don't think that scenario matches up. I was asking for an example where the user wants to find the code, but doesn't know how many layers deep it is. Not an example where the user might wrongly expect it to be at the top-level. In this example you gave, why would they use this "recursively find code" function as opposed to directly accessing the code?

That's what I was getting at - that if the user expects a code at all, it's likely because they are checking for a specific error condition which would have the code in a known location.

It sounds like you're saying users might get tripped up here by confusing our readFile with fs.promises.readFile. And that by making ours behave more like fs.promises.readFile we'd avoid confusion. Totally agreed there, that makes perfect sense.

Copy link
Member

@Gudahtt Gudahtt Aug 2, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I opted to use pony-cause instead of error-cause as I didn't want to change any globals.

That makes sense. Though we could get away with using neither. In Node.js v14, the cause property doesn't cause the Error constructor to blow up or anything. It just gets ignored. Not ideal of course, but considering that we don't expect this to get used on Node.js v14 in practice, maybe we don't care? The wrapper error with the stack probably provides enough context in most cases anyway, especially if the code is being set on the wrapper error.

Using pony-cause has advantages though. It avoids a potential foot-gun if we ever have runtime checks for cause. And we might want to use a similar pattern in libraries that will be run with Node.js v14.

src/env-utils.test.ts Outdated Show resolved Hide resolved
src/misc-utils.ts Outdated Show resolved Hide resolved
src/misc-utils.ts Show resolved Hide resolved
package.json Outdated Show resolved Hide resolved
src/file-utils.test.ts Outdated Show resolved Hide resolved
src/command-line-arguments.ts Show resolved Hide resolved
tests/unit/helpers.ts Outdated Show resolved Hide resolved
mcmire and others added 3 commits August 1, 2022 09:54
Co-authored-by: Mark Stacey <markjstacey@gmail.com>
Co-authored-by: Mark Stacey <markjstacey@gmail.com>
src/package.ts Show resolved Hide resolved
src/package-manifest.ts Outdated Show resolved Hide resolved
mcmire and others added 2 commits August 1, 2022 15:37
Co-authored-by: Mark Stacey <markjstacey@gmail.com>
src/package-manifest.ts Outdated Show resolved Hide resolved
src/package-manifest.ts Outdated Show resolved Hide resolved
src/monorepo-workflow-operations.test.ts Outdated Show resolved Hide resolved
src/monorepo-workflow-operations.ts Outdated Show resolved Hide resolved
Gudahtt
Gudahtt previously approved these changes Aug 2, 2022
Copy link
Member

@Gudahtt Gudahtt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

There is one outstanding discussion regarding wrapError, but that doesn't need to block.The PR as a whole is in fantastic shape.

I didn't review the workflow operations tests line-by-line, just skimmed to get a general sense. I'll look at those more closely in the follow-up PRs that affect those I'm sure.

Gudahtt
Gudahtt previously approved these changes Aug 2, 2022
return error;
}

return new Error(`${message}: ${originalError}`);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: imo it is printed more nicely if ErrorWithCause is used, even for non-error-types. cause doesn't have to be an error type. It would simplify this function to remove that case as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, it seems that the type for ErrorWithCause is wrong in pony-cause, although to be fair, this is because it's trying to be compatible with TypeScript and the type for Error is wrong there too. This was fixed here and it looks like it'll be available in TypeScript 4.8. I could patch pony-cause, but I think that would only apply to development — the types would be broken in production. So I think we should think about this a little bit more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That issue seems to indicate that updating TypeScript would fix the issue. We can do that later.

* something throwable).
* @returns A new error object.
*/
export function coverError(message: string, originalError: unknown) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: why the change in name? wrapError seemed appropriate, and is what I've always heard this called. The spec uses the verb "wrap" for this pattern as well.

Copy link
Member

@Gudahtt Gudahtt Aug 2, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Though I guess in the general case of wrapping errors, we can use ErrorWithCause directly, and there would be no need for this function to exist. Except that it passes along the inner code. Was the name change related to the code behaviour?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problem. Changed in b4d820d.

@mcmire
Copy link
Contributor Author

mcmire commented Aug 2, 2022

Okay one more :)

@mcmire mcmire merged commit 603d122 into main Aug 2, 2022
@mcmire mcmire deleted the add-first-flow branch August 2, 2022 22:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants