-
-
Notifications
You must be signed in to change notification settings - Fork 6
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
Conversation
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
There was a problem hiding this 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', |
There was a problem hiding this comment.
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/git-utils.ts
Outdated
* @returns The HTTPS URL of the repository, e.g. | ||
* `https://github.com/OrganizationName/RepositoryName`. | ||
*/ | ||
export async function getRepositoryHttpsUrl( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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/package-manifest-utils.ts
Outdated
@@ -0,0 +1,348 @@ | |||
import path from 'path'; |
There was a problem hiding this comment.
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.
src/release-specification-utils.ts
Outdated
@@ -0,0 +1,298 @@ | |||
import fs, { WriteStream } from 'fs'; |
There was a problem hiding this comment.
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.
src/workflow-utils.ts
Outdated
@@ -0,0 +1,74 @@ | |||
import { Package } from './package-utils'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is new stuff.
src/package-utils.ts
Outdated
* @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({ |
There was a problem hiding this comment.
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
.
src/monorepo-workflow-utils.ts
Outdated
@@ -0,0 +1,244 @@ | |||
import type { WriteStream } from 'fs'; |
There was a problem hiding this comment.
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.
Also I know that there are linting errors, I will address those tomorrow 😓 |
src/package-manifest-utils.ts
Outdated
validation: validationForManifestDependenciesField, | ||
defaultValue: {}, | ||
}); | ||
return { ...obj, [fieldName]: dependencies }; |
There was a problem hiding this comment.
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.
src/package-manifest-utils.ts
Outdated
[ManifestFieldNames.Version]: version, | ||
[ManifestFieldNames.Workspaces]: workspaces, | ||
[ManifestFieldNames.Private]: privateValue, | ||
...dependencyFields, |
There was a problem hiding this comment.
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.
There was a problem hiding this 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/misc-utils.ts
Outdated
* returns that `message` by default. | ||
* @returns A new error object. | ||
*/ | ||
export function wrapError( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
>
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 thecode
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.
There was a problem hiding this comment.
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 oferror-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.
Co-authored-by: Mark Stacey <markjstacey@gmail.com>
Co-authored-by: Mark Stacey <markjstacey@gmail.com>
Co-authored-by: Mark Stacey <markjstacey@gmail.com>
Co-authored-by: Mark Stacey <markjstacey@gmail.com>
There was a problem hiding this 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.
return error; | ||
} | ||
|
||
return new Error(`${message}: ${originalError}`); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
src/misc-utils.ts
Outdated
* something throwable). | ||
* @returns A new error object. | ||
*/ | ||
export function coverError(message: string, originalError: unknown) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
Okay one more :) |
This commit introduces a rough cut of a flow which supports a monorepo
with an independent versioning strategy. This flow:
workspace packages within the repo (this will be changed to all
updated packages in a future commit)
edit it
(note: the format of the version string will be changed in a future
commit)
spec
the release spec