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

Rework publishing to work with all popular package managers #674

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

Andarist
Copy link
Member

@Andarist Andarist commented Nov 20, 2021

fixes #598
fixes #432

at the moment this is being opened as a draft, to gather initial feedback

The goal of this PR is to delegate as much as possible to yarn berry / pnpm so they can do their magic with rewriting package manifests and stuff

TODO:

cc @schickling @zkochan (might be interesting to you)

@changeset-bot
Copy link

changeset-bot bot commented Nov 20, 2021

🦋 Changeset detected

Latest commit: 7608211

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@changesets/cli Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copy link
Member

@emmatown emmatown left a comment

Choose a reason for hiding this comment

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

Maybe we should log stdout and stderr when publish fails so people can debug when something goes wrong like we fail to parse the json? (and maybe even land that and the getLastJsonObjectFromString stuff separately to package manager changes?)

[
...publishTool.args,
opts.cwd,
"--json",
Copy link

Choose a reason for hiding this comment

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

Looks like if yarn 2+ does not support --json, the argument should be conditionally added

Copy link
Member Author

Choose a reason for hiding this comment

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

yes, thank you for spotting that - this is an obvious oversight on my part, I knew that this ain't supported as it makes me handle yarn berry in a different way just a few lines later

just out of curiosity - is using an unknown flag makes yarn throw an error?

Copy link

Choose a reason for hiding this comment

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

Unfortunately yes, just tested with your current P/R to be sure (yarn 3.2.0-rc.5):

soluble-io/cache-interop#222 (comment)

BTW I love the butterfly in the yarn output, made me smile :)

🦋  error Unknown Syntax Error: Extraneous positional argument ("/home/sebastien/github/cache-interop/packages/cache-redis").

Copy link
Member Author

Choose a reason for hiding this comment

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

I think you have tested the other case 😅 but this one doesn't work either:

Unknown Syntax Error: Unsupported option name ("--json").

Choose a reason for hiding this comment

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

Yep 😅, I've been struggling a bit with how to build your P/R to use it...

But that definitely happened and it was nice to see.

Copy link
Member Author

Choose a reason for hiding this comment

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

I will make sure to setup snapshot releases for PRs before we go into re-testing this after I apply the suggested changes

["publish", opts.cwd, "--json", ...publishFlags],
[
...publishTool.args,
opts.cwd,
Copy link

Choose a reason for hiding this comment

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

opts.cwd

I don't think it will work with yarn 2+, not sure about the other package managers (https://yarnpkg.com/cli/npm/publish has no reference to path)

I think this should work

 const args = [
    ...publishTool.args,
   // "--json",  // <- assuming here we add --json for naything !== yarn 2+
    ...publishFlags,
    ...publishTool.flags
  ];
  let { code, stdout, stderr } = await spawn(
    publishTool.name,
    args,
    {
      // Here we can send the current working directory
      cwd: opts.cwd,
      env: Object.assign({}, process.env, envOverride)
    }
  );

Just tested it and it works on yarn 3. But I'm wondering if it would be cleaner to extract the command and add tests on it. I'll have a look tomorrow to see if I can help

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah, yes - good catch. I got Unknown Syntax Error: Extraneous positional argument ("packages/test"). with this.

I think it's safe to just use { cwd: opts.cwd } for all package managers.

Copy link
Member Author

Choose a reason for hiding this comment

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

Just tested it and it works on yarn 3. But I'm wondering if it would be cleaner to extract the command and add tests on it. I'll have a look tomorrow to see if I can help

Tests for this whole thing would be great. I'm just unsure how to test this best - we really should have some e2e tests here because there are so many little details hidden within those publish commands that we really should invoke those and verify what can be observed somehow.

Copy link

Choose a reason for hiding this comment

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

Totally. I would be lazy and just unit test something in the lines of

type Params= {
   pm: 'pnpm' | 'yarn' | 'npm',
   pmVersion?: string,
   tag?: string,
   access?: 'public' | 'private',
   otp?: string
}
const getPublishSpawnParams = (params: Params): {cmd: string, args: string[]} => {
 ///....
}

describe('when pnpm', () => {
   it('should return a pnpm..', () => {
      expect(getPublishSpawnParams({pm: 'pnpm', pmVersion: '6.4' tag: 'main', access: 'public', otp: 'xxx'}).toStrictEqual({
          cmd: 'pnpm',
          args: ['publish', '--access', 'public', '--tag', 'main', '--json', '--no-git-checks']
      })
   }); 
}

describe('when yarn < 2', () => {
   it('should return a npm publish..', () => {
      expect(getPublishSpawnParams({pm: 'yarn', pmVersion: '8.0', tag: 'main', access: 'public', otp: 'xxx'}).toStrictEqual({
          cmd: 'npm',
          args: ['publish', '--access', 'public', '--tag', 'main' '--otp', 'xxx']
      })
   }); 
}

describe('when yarn 2+', () => {
   it('should return a yarn..', () => {
      expect(getPublishSpawnParams({pm: 'yarn', pmVersion: '2.4', tag: 'main', access: 'public', otp: 'xxx'}).toStrictEqual({
          cmd: 'yarn',
          args: ['npm', 'publish', '--access', 'public', '--tag', 'main']
      })
   }); 
}

describe('when yarn 3.2', () => {
   it('should return a yarn..', () => {
      expect(getPublishSpawnParams({pm: 'yarn', pmVersion: "3.2', tag: 'main', access: 'public', otp: 'xxx'}).toStrictEqual({
          cmd: 'yarn',
          // I saw that you've opened a --otp option in yarn, so why not ?
          args: ['npm', 'publish', '--access', 'public', '--tag', 'main', '-otp', 'xxx']
      })
   }); 
}

// + some extras 

Not so sure about how to mock spawn, so that might make sense

I'm bad with the names hey :)

Choose a reason for hiding this comment

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

I'm just checking and you're totally right so much small details, that's insane :)

So definitely I would live well with unit tests cause e2e looks a huge effort, especially with minor versions.

I would start with some specs for the most common cases (npm, yarn 1/2+, pnpm 6) and live with a "dirty" function that can be improved later (with things like https://www.npmjs.com/package/compare-versions to adapt the result)

@Andarist
Copy link
Member Author

@belgattitude thank you very much for testing this ❤️ I will make necessary changes here soon, I would only want to land this first #676 so I could rebase this one on top of it.

Andarist and others added 3 commits November 26, 2021 14:31
…e path as argument to the command. This makes sure that it will work with Yarn Berry

Co-authored-by: Sébastien Vanvelthem <belgattitude@users.noreply.github.com>
Co-authored-by: Sébastien Vanvelthem <belgattitude@users.noreply.github.com>
@Andarist
Copy link
Member Author

@belgattitude I've rebased this and applied your suggestions. I've also created a PR to add snapshot releases (here). This can only be merged in when we confirm that it has been done in a secure way, so I don't want to rush it. If you could retest this PR "manually" (like you have done before) - I would highly appreciate it.

@belgattitude
Copy link

belgattitude commented Nov 30, 2021

Hey @Andarist thanks a lot. Here it is

yarn changeset publish
sebastien@seb-thinkpad-x1:~/github/cache-interop$ yarn changeset publish
🦋  warn ===============================IMPORTANT!===============================
🦋  warn You are in prerelease mode so packages will be published to the canary
🦋  warn         dist tag except for packages that have not had normal releases which will be published to latest
🦋  warn ----------------------------------------------------------------------
🦋  info npm info @soluble/cache-interop
🦋  info npm info @soluble/cache-ioredis
🦋  info npm info @soluble/cache-redis
🦋  info npm info @soluble/dsn-parser
🦋  info @soluble/cache-interop is being published because our local version (0.8.2-canary.0) has not been published on npm
🦋  info @soluble/cache-ioredis is being published because our local version (0.8.4-canary.0) has not been published on npm
🦋  info @soluble/cache-redis is being published because our local version (0.8.4-canary.0) has not been published on npm
🦋  info @soluble/dsn-parser is being published because our local version (1.3.3-canary.0) has not been published on npm
🦋  info Publishing "@soluble/cache-interop" at "0.8.2-canary.0"
🦋  info Publishing "@soluble/cache-ioredis" at "0.8.4-canary.0"
🦋  info Publishing "@soluble/cache-redis" at "0.8.4-canary.0"
🦋  info Publishing "@soluble/dsn-parser" at "1.3.3-canary.0"
🦋  info will publish with 'yarn npm publish --access public --tag canary'
🦋  info will publish with 'yarn npm publish --access public --tag canary'
🦋  info will publish with 'yarn npm publish --access public --tag canary'
🦋  info will publish with 'yarn npm publish --access public --tag canary'
🦋  error an error occurred while publishing @soluble/dsn-parser: 
🦋  error ➤ YN0033: No authentication configured for request
🦋  error an error occurred while publishing @soluble/cache-ioredis: 
🦋  error ➤ YN0033: No authentication configured for request
🦋  error an error occurred while publishing @soluble/cache-redis: 
🦋  error ➤ YN0033: No authentication configured for request
🦋  error an error occurred while publishing @soluble/cache-interop: 
🦋  error ➤ YN0033: No authentication configured for request
🦋  error packages failed to publish:
🦋  @soluble/cache-interop@0.8.2-canary.0
🦋  @soluble/cache-ioredis@0.8.4-canary.0
🦋  @soluble/cache-redis@0.8.4-canary.0
🦋  @soluble/dsn-parser@1.3.3-canary.0

The command is not complaining anymore with yarn 3.2.0-rc.6.

🦋 info will publish with 'yarn npm publish --access public --tag canary'

PS: I've added the log here: https://github.com/atlassian/changesets/pull/682/files just to be sure.

That said it's not a full publish, I'll need --otp for that and can only publish my repo from a gh action for now.

So all look fine. My concerns are related to

  • Logging: As it look probable we run into bugs, I would add some logging about the command used.
  • Prevent regressions: I feel we should still have some kind of unit tests for the command (and pm versions). To ensure future changes does not break things.

Of course all is up to you, I come from different ecosystem so my reflexes often lack perspective and love to learn and share in general.

Let me know I still have few free days, and I'll gladly help.

@Andarist
Copy link
Member Author

Logging: As it look probable we run into bugs, I would add some logging about the command used.

That's probably a good call - anything that you would like to see logged beside the information added in your PR here?

Prevent regressions: I feel we should still have some kind of unit tests for the command (and pm versions). To ensure future changes does not break things.

I agree that we need some tests here - I'm just on the fence when it comes to unit tests here. The logic itself is pretty straightforward - the only problem is that I'm not always sure what exact combination of options has to be passed in a specific situation. And when it comes to that I think that comments can be more valuable than unit tests.

I just kinda feel that I would be that more confident about this logic if I would have those unit tests. I'm not a testing expert though and probably many people would say that this would be worthwhile - I'm just not feeling that. Maybe @mitchellhamilton would have some additional opinion about this.

The command is not complaining anymore with yarn 3.2.0-rc.6.

Just to make sure that we are on the same page - everything looks OK now from your end, it's just that publish fails because OTP is required and you are not using the automation token because you are running the publish script locally and not on the CI. Right?

@emmatown
Copy link
Member

testing

tbh, what I'd really like is tests that run with all the different package managers against a local registry (probably https://verdaccio.org) with a way to run the tests against actual registries.

@Andarist
Copy link
Member Author

Andarist commented Dec 1, 2021

Let's move the tests-related discussion to the dedicated thread. I don't think this is sort of out of the scope of this PR and should be addressed separately.

@Andarist Andarist marked this pull request as ready for review December 1, 2021 10:48
@belgattitude
Copy link

belgattitude commented Dec 1, 2021

anything that you would like to see logged beside the information added in your PR here?

New to the project so I don't know but I would follow the concerns from #674 (review) @mitchellhamilton

Just to make sure that we are on the same page - everything looks OK now from your end, it's just that publish fails because OTP is required

Yep. Don't want to mess too much my repo 😄. A canary of changeset can help for me to test it out fully, but as the "yarn command" is okay, I feel I can be confident enough the tool will do its job.

About tests

Just want to express something about unit. I'm certainly not trying to create a debate, just sharing and if it's not useful, please forget.

  1. Don't test what you don't own.

    I like the idea of e2e, but in this specific case I would be confident that:

    • given the package manager cli receive the correct params it will do the job or it's a bug on their side. They have an e2e suite that runs on many os... spawn (which I don't know about) has it's own tests too, I guess. So for me what is to test is the command we run, less the fact that npm, pnpm, yarn 1.2.3 will actually publish what what we are feeding to it. Command line is somehow a contract. Same idea when I test a server, do I need to test if my express server works or just my handler/use-case (business...) .... There's not big difference when you write the tests, but running them is much easier when you don't need to start the actual express server (time, flacky...).
  2. What kind of unit tests

    For example we could list the commands that are expected by each tool (from their doc). To illustrate (I haven't checked just taken from what I read in this P/R)

    Given Expected
    Yarn 1 --otp npm publish --access public --tag main --otp xxx --json
    Yarn 1 npm publish --access public --tag main --json
    Yarn 2 yarn npm publish --access public --tag main
    Yarn 2 --otp should throw 'Unsupported --otp, upgrade to yarn 3.2+'
    Yarn 3.2 --otp yarn npm publish --access public --tag main --otp xxx
    Pnpm 6 yarn npm publish --access public --tag main --json --no-git-checks
    Npm 6 --otp npm publish --access public --tag main --otp xxx --json
    Npm 8 --otp npm publish --access public --tag main --otp xxx --json

    That could be written in many ways... In this scenario looks a fit for kind of acceptance / BDD / cucumber style. either given/when/expect like here or even with a simple it.each and an array of scenarios.

    That should suffice to prevent regressions, as you said the code is trivial but how much variations will come in the future. Personally as a contributor (If I become, I'll become 😄 ) I would be more confident to make changes.

If you like I could add an example test file in a separate P/R... The only thing is that I don't really know node, so a reviewer/guide will be needed 😄

Hey thanks again

@belgattitude
Copy link

@Andarist a little addition. I'm not sure you're aware of it. When I faced the issue with yarn/pnpm workspace: protocol, I just did a quick hack for my own use case. Rather than change npm for publication, I intercepted the pack step (yarn pack, pnpm pack) to generate the archive. see here #585 (comment).

I'm thinking of it now, I guess it's a bit late to tell and I don't want to add confusion here. So feel free to ignore, but it might help to harmonize and have one tool to test for publishing.

@ragingwind
Copy link

@mitchellhamilton any updates?

@Andarist
Copy link
Member Author

Andarist commented Jan 4, 2022

@ragingwind I still have some minor things to adjust here and to figure out the authentication story in changesets/action. I didn't have much time to spend on this lately - but I will be working on landing this PR as soon as I can.

Also, note that you can try out some prerelease versions that include this PR and get back to me with the feedback. This would actually be very helpful. If you want to do that - let me know and I will prepare some installation instructions for you (please just mention which package manager are you currently using).

@ragingwind
Copy link

@Andarist Thank you. It would be great I could use it with prerelease. We use yarn@3.1.0 now

@ilyaGurevich
Copy link

@Andarist definitely looking forward to this PR, we are attempting to upgrade our monorepo from yarn v1 to yarn v3 and would like to also retain our conventional-commits/semantic releases that lerna used to give but is no longer compatible with yarn berry

@Andarist
Copy link
Member Author

@ragingwind I would say that at the moment using this packed version from CodeSandbox CI is the easiest:

@changesets/cli@https://pkg.csb.dev/changesets/changesets/commit/98401c27/@changesets/cli/_pkg.tgz

Since you are using Yarn 3 you'd have to use this Yarn plugin to resolve requested dependencies correctly.

You could also try installing this version from npm:

@changesets/cli@2.19.0-temp.0

but it's a little behind the one available on CodeSandbox CI - and any future prereleases like that will only be available through CodeSandbox CI so that's what I recommend now.

Note that if you are also using changesets/actions then you should authenticate correctly before calling our action, otherwise Yarn won't be able to authenticate publish requests. I'm a little bit worried that npm info calls might fail either way - but if you could confirm that then I would have an actionable item to fix as part of this PR.

and would like to also retain our conventional-commits/semantic releases that lerna used to give

@ilyaGurevich note that how Changesets work is slightly different from that - the generated changelog updates etc are not based on commit messages but on the committed changeset .md files

@github-actions github-actions bot mentioned this pull request Feb 19, 2022
@unional
Copy link

unional commented Jun 7, 2022

Also notice that changeset is reading registry settings from .npmrc instead of .yarnrc.yml even the repo is using yarn instead of npm.

#813

@7rulnik
Copy link

7rulnik commented Jul 1, 2022

@Andarist do you need some additional help to finish it?

@gamertike
Copy link

hw is this pr going and when will i be able to use this? atm moment i really need this because my publishing automation for my project is broken due to yarn not being supported.

@terenceodonoghue
Copy link

Just checking on this PR, keen to see it finished. @Andarist if you need any help (or sponsorship) to see it through, I'd be happy to help, it's an awesome tool otherwise. 🦋

@scottdickerson
Copy link

are there any updates on this, we really miss the great capabilities of yarn changeset publish

@tianyingchun
Copy link

how about this issue progress? really need this feature without any hack.

@tianyingchun
Copy link

tianyingchun commented Oct 22, 2022

@Andarist

  1. try using @changesets/cli@2.22.0-temp.0 publish failed.
  2. using @changesets/cli@2.25.0 it works, but package dependencies wrong keep :workpsace:^

➤ YN0000: Done in 7s 668ms
🦋 info npm info @armit/cli
🦋 info npm info @armit/common
🦋 info npm info @armit/eslint-config-bases
🦋 info @armit/cli is being published because our local version (0.0.5) has not been published on npm
🦋 info @armit/common is being published because our local version (0.0.5) has not been published on npm
🦋 info @armit/eslint-config-bases is being published because our local version (0.0.4) has not been published on npm
🦋 info Publishing "@armit/cli" at "0.0.5"
🦋 info Publishing "@armit/common" at "0.0.5"
🦋 info Publishing "@armit/eslint-config-bases" at "0.0.4"
🦋 error an error occurred while publishing @armit/eslint-config-bases:
🦋 error ➤ YN0000: CHANGELOG.md
🦋 error ➤ YN0000: README.md
🦋 error ➤ YN0000: package.json
🦋 error ➤ YN0000: src/bases/index.js
🦋 error ➤ YN0000: src/bases/jest.js
🦋 error ➤ YN0000: src/bases/playwright.js
🦋 error ➤ YN0000: src/bases/prettier.js
🦋 error ➤ YN0000: src/bases/react.js
🦋 error ➤ YN0000: src/bases/regexp.js
🦋 error ➤ YN0000: src/bases/rtl.js
🦋 error ➤ YN0000: src/bases/sonar.js
🦋 error ➤ YN0000: src/bases/storybook.js
🦋 error ➤ YN0000: src/bases/tailwind.js
🦋 error ➤ YN0000: src/bases/typescript.js
🦋 error ➤ YN0000: src/bases/vitest.js
🦋 error ➤ YN0000: src/helpers/getDefaultIgnorePatterns.js
🦋 error ➤ YN0000: src/helpers/getPrettierConfig.js
🦋 error ➤ YN0000: src/helpers/index.js
🦋 error ➤ YN0000: src/index.js
🦋 error ➤ YN0000: src/patch/modern-module-resolution.js
🦋 error ➤ YN0000: src/prettier.base.config.js
🦋 error ➤ YN0033: No authentication configured for request
🦋 error an error occurred while publishing @armit/cli:
🦋 error ➤ YN0000: CHANGELOG.md
🦋 error ➤ YN0000: LICENSE
🦋 error ➤ YN0000: README.md
🦋 error ➤ YN0000: bin/armit.mjs
🦋 error ➤ YN0000: dist/index.d.ts
🦋 error ➤ YN0000: dist/index.js
🦋 error ➤ YN0000: dist/index.js.map
🦋 error ➤ YN0000: index.js
🦋 error ➤ YN0000: package.json
🦋 error ➤ YN0033: No authentication configured for request
🦋 error an error occurred while publishing @armit/common:
🦋 error ➤ YN0000: CHANGELOG.md
🦋 error ➤ YN0000: LICENSE
🦋 error ➤ YN0000: README.md
🦋 error ➤ YN0000: dist/index.d.ts
🦋 error ➤ YN0000: dist/index.js
🦋 error ➤ YN0000: dist/index.js.map
🦋 error ➤ YN0000: index.js
🦋 error ➤ YN0000: package.json
🦋 error ➤ YN0033: No authentication configured for request
🦋 error packages failed to publish:
🦋 @armit/cli@0.0.5
🦋 @armit/common@0.0.5
🦋 @armit/eslint-config-bases@0.0.4
Error: The process '/usr/local/bin/yarn' failed with exit code 1
at m._setResult (/home/runner/work/_actions/changesets/action/v1/dist/index.js:102:7258)
Error: The process '/usr/local/bin/yarn' failed with exit code 1
at m.CheckComplete (/home/runner/work/_actions/changesets/action/v1/dist/index.js:102:6686)
at ChildProcess. (/home/runner/work/_actions/changesets/action/v1/dist/index.js:102:5723)
at ChildProcess.emit (events.js:314:20)
at maybeClose (internal/child_process.js:1022:16)
at Process.ChildProcess._handle.onexit (internal/child_process.js:287:5)

@HosseinAgha
Copy link

HosseinAgha commented Nov 14, 2022

@Andarist could you please rebase with the main branch? I need the latest main branch changes in the csb/snapshot releases.

@jmatsushita
Copy link

I've been trying out this PR with @qnighy's patch and with yarn 4.0.0-rc.45

I get the following error message when running yarn changeset publish:

🦋  error error while checking if token is required npm ERR! code E401
🦋  error npm ERR! 401 Unauthorized - GET https://registry.npmjs.org/-/npm/v1/user
🦋  error {
🦋  error   "error": {
🦋  error     "code": "E401",
🦋  error     "summary": "401 Unauthorized - GET https://registry.npmjs.org/-/npm/v1/user",
🦋  error     "detail": ""
🦋  error   }
🦋  error }
🦋  error
🦋  error npm ERR! A complete log of this run can be found in:
🦋  error npm ERR!     /Users/jun/.npm/_logs/2023-06-09T10_56_34_917Z-debug-0.log

I traced this to the fact that getTokenIsRequired() is still in the hotpath, but that would require npm profile get --json to work, whereas it's not configured (since yarn uses .yarnrc.yml for registry configuration.

If I simply short circuit this function and return false; then it works. A temporary fix of course, but I hope this would help and maybe give back a tiny bit of momentum to get this over the line. Happy to test new patches!

@jmatsushita
Copy link

jmatsushita commented Jun 10, 2023

Just bumped into the fact that for changesets to check if the package is published in a registry, it uses getPackageInfo which is still using npm info instead of yarn npm info. In the case where registry access is configured only through a .yarnrc.yml file this will fail with a 404 (since it doesn't use the proper registry for that @scope) and will try to republish the package which is already in the registry, and fail if the package was already published (since the yarn npm publish part works correctly).

🦋  warn Received 404 for npm info "@scope/package"
🦋  info @scope/package is being published because our local version (0.0.0) has not been published on npm
🦋  info Publishing "@scope/package" at "0.0.0"
🦋  error an error occurred while publishing @scope/package: 
🦋  error ➤ YN0000: output/Main/index.js
🦋  error ➤ YN0000: package.json
🦋  error ➤ YN0035: The remote server failed to provide the requested resource
🦋  error ➤ YN0035:   Response Code: [40](https://github.com/org/repo/actions/runs/.../jobs/...#step:12:41)3 (Forbidden)
🦋  error ➤ YN0035:   Request Method: PUT
🦋  error ➤ YN0035:   Request URL: https://gitlab.com/api/v4/projects/.../packages/npm/@scope%2fpackage
🦋  error packages failed to publish:
🦋  @scope/package@0.0.0

A similar logic than internalPublish but for yarn npm info works

let { code, stdout, stderr } =
publishTool.name === "yarn"
? await spawn("yarn", ["npm", "publish", ...publishFlags], {
cwd: opts.cwd,
env: Object.assign({}, process.env, envOverride)
})
: await spawn(publishTool.name, ["publish", opts.cwd, ...publishFlags], {
env: Object.assign({}, process.env, envOverride)
});

Here's an updated version of the above patch which works in yarn 4.0.0-rc.45 with these incantations in the package.json:

{
  "name": "package",
  "version": "0.0.0",
  "devDependencies": {
    "@changesets/cli": "patch:@changesets/cli@npm%3A2.26.0#~/.yarn/patches/@changesets-cli-npm-2.26.0-49d5c5f72d.patch"
  },
  "packageManager": "yarn@4.0.0-rc.45",
  "resolutions": {
    "@changesets/cli@^2.26.0": "patch:@changesets/cli@npm%3A2.26.0#./.yarn/patches/@changesets-cli-npm-2.26.0-49d5c5f72d.patch"
  },
}
.yarn/patches/@changesets-cli-npm-2.26.0-49d5c5f72d.patch
diff --git a/dist/cli.cjs.dev.js b/dist/cli.cjs.dev.js
index b1582198d3d2631569ff59f27170354a932d3ad0..2826a10919ca861e82efa105b6604fa52eda9847 100644
--- a/dist/cli.cjs.dev.js
+++ b/dist/cli.cjs.dev.js
@@ -1,5 +1,4 @@
 'use strict';
-
 var meow = require('meow');
 var errors = require('@changesets/errors');
 var logger = require('@changesets/logger');
@@ -705,31 +704,33 @@ function getCorrectRegistry(packageJson) {
   return !registry || registry === "https://registry.yarnpkg.com" ? "https://registry.npmjs.org" : registry;
 }
 
+const getPublishToolVersion = async (name, cwd) => (await spawn__default['default'](name, ["--version"], {
+  cwd
+})).stdout.toString().trim();
+
 async function getPublishTool(cwd) {
-  const pm = await preferredPM__default['default'](cwd);
-  if (!pm || pm.name !== "pnpm") return {
-    name: "npm"
-  };
+  var _await$preferredPM;
 
-  try {
-    let result = await spawn__default['default']("pnpm", ["--version"], {
-      cwd
-    });
-    let version = result.stdout.toString().trim();
-    let parsed = semver__default['default'].parse(version);
-    return {
-      name: "pnpm",
-      shouldAddNoGitChecks: (parsed === null || parsed === void 0 ? void 0 : parsed.major) === undefined ? false : parsed.major >= 5
-    };
-  } catch (e) {
+  const name = ((_await$preferredPM = await preferredPM__default['default'](cwd)) === null || _await$preferredPM === void 0 ? void 0 : _await$preferredPM.name) || "npm";
+  const version = await getPublishToolVersion(name, cwd);
+
+  if (name === "yarn" && semver__default['default'].lt(version, "2.0.0")) {
+    // Yarn Classic doesn't do anything special when publishing, let's stick to the npm client in such a case
     return {
-      name: "pnpm",
-      shouldAddNoGitChecks: false
+      name: "npm",
+      version: await getPublishToolVersion("npm", cwd)
     };
   }
+
+  return {
+    name,
+    version
+  };
 }
 
 async function getTokenIsRequired() {
+  let publishTool = await getPublishTool(process.cwd());
+  if (publishTool.name === "yarn") return false;
   // Due to a super annoying issue in yarn, we have to manually override this env variable
   // See: https://github.com/yarnpkg/yarn/issues/2935#issuecomment-355292633
   const envOverride = {
@@ -754,6 +755,7 @@ async function getTokenIsRequired() {
 }
 function getPackageInfo(packageJson) {
   return npmRequestLimit(async () => {
+    let publishTool = await getPublishTool(process.cwd());
     logger.info(`npm info ${packageJson.name}`); // Due to a couple of issues with yarnpkg, we also want to override the npm registry when doing
     // npm info.
     // Issues: We sometimes get back cached responses, i.e old data about packages which causes
@@ -761,7 +763,9 @@ function getPackageInfo(packageJson) {
     // as they will always give a 404, which will tell `publish` to always try to publish.
     // See: https://github.com/yarnpkg/yarn/issues/2935#issuecomment-355292633
 
-    let result = await spawn__default['default']("npm", ["info", packageJson.name, "--registry", getCorrectRegistry(packageJson), "--json"]); // Github package registry returns empty string when calling npm info
+    let result = publishTool.name === "yarn" ? await spawn__default['default']("yarn", ["npm", "info", packageJson.name, "--json"], {
+      env: Object.assign({}, process.env)
+    }) : await spawn__default['default']("npm", ["info", packageJson.name, "--registry", getCorrectRegistry(packageJson), "--json"]); // Github package registry returns empty string when calling npm info
     // for a non-existent package instead of a E404
 
     if (result.stdout.toString() === "") {
@@ -816,20 +820,32 @@ let getOtpCode = async twoFactorState => {
   }
 
   return askForOtpCode(twoFactorState);
+};
+
+const isOtpError = error => {
+  // The first case is no 2fa provided, the second is when the 2fa is wrong (timeout or wrong words)
+  return error.code === "EOTP" || error.code === "E401" && error.detail.includes("--otp=<code>");
 }; // we have this so that we can do try a publish again after a publish without
 // the call being wrapped in the npm request limit and causing the publishes to potentially never run
 
+
 async function internalPublish(pkgName, opts, twoFactorState) {
   let publishTool = await getPublishTool(opts.cwd);
-  let publishFlags = opts.access ? ["--access", opts.access] : [];
+  let shouldHandleOtp = !isCI__default['default'] && (publishTool.name === "yarn" ? semver__default['default'].gte(publishTool.version, "3.2.0-rc.8") : true);
+  let publishFlags = publishTool.name !== "yarn" ? ["--json"] : [];
+
+  if (opts.access) {
+    publishFlags.push("--access", opts.access);
+  }
+
   publishFlags.push("--tag", opts.tag);
 
-  if ((await twoFactorState.isRequired) && !isCI__default['default']) {
+  if (shouldHandleOtp && (await twoFactorState.isRequired)) {
     let otpCode = await getOtpCode(twoFactorState);
     publishFlags.push("--otp", otpCode);
   }
 
-  if (publishTool.name === "pnpm" && publishTool.shouldAddNoGitChecks) {
+  if (publishTool.name === "pnpm" && semver__default['default'].gte(publishTool.version, "5.0.0")) {
     publishFlags.push("--no-git-checks");
   } // Due to a super annoying issue in yarn, we have to manually override this env variable
   // See: https://github.com/yarnpkg/yarn/issues/2935#issuecomment-355292633
@@ -842,21 +858,35 @@ async function internalPublish(pkgName, opts, twoFactorState) {
     code,
     stdout,
     stderr
-  } = await spawn__default['default'](publishTool.name, ["publish", opts.cwd, "--json", ...publishFlags], {
+  } = publishTool.name === "yarn" ? await spawn__default['default']("yarn", ["npm", "publish", ...publishFlags], {
+    cwd: opts.cwd,
+    env: Object.assign({}, process.env, envOverride)
+  }) : await spawn__default['default'](publishTool.name, ["publish", opts.cwd, ...publishFlags], {
     env: Object.assign({}, process.env, envOverride)
   });
 
   if (code !== 0) {
-    // NPM's --json output is included alongside the `prepublish` and `postpublish` output in terminal
+    // yarn berry doesn't support --json and we don't attempt to parse its output to a machine-readable format
+    if (publishTool.name === "yarn") {
+      const output = stdout.toString().trim().split("\n") // this filters out "unnamed" logs: https://yarnpkg.com/advanced/error-codes/#yn0000---unnamed
+      // this includes a list of packed files and the "summary output" like: "Failed with errors in 0s 75ms"
+      // those are not that interesting so we reduce the noise by dropping them
+      .filter(line => !/YN0000:/.test(line)).join("\n");
+      logger.error(`an error occurred while publishing ${pkgName}:`, `\n${output}`);
+      return {
+        published: false
+      };
+    } // NPM's --json output is included alongside the `prepublish` and `postpublish` output in terminal
     // We want to handle this as best we can but it has some struggles:
     // - output of those lifecycle scripts can contain JSON
     // - npm7 has switched to printing `--json` errors to stderr (https://github.com/npm/cli/commit/1dbf0f9bb26ba70f4c6d0a807701d7652c31d7d4)
     // Note that the `--json` output is always printed at the end so this should work
+
+
     let json = getLastJsonObjectFromString(stderr.toString()) || getLastJsonObjectFromString(stdout.toString());
 
     if (json !== null && json !== void 0 && json.error) {
-      // The first case is no 2fa provided, the second is when the 2fa is wrong (timeout or wrong words)
-      if ((json.error.code === "EOTP" || json.error.code === "E401" && json.error.detail.includes("--otp=<code>")) && !isCI__default['default']) {
+      if (shouldHandleOtp && isOtpError(json.error)) {
         if (twoFactorState.token !== null) {
           // the current otp code must be invalid since it errored
           twoFactorState.token = null;
diff --git a/dist/cli.cjs.prod.js b/dist/cli.cjs.prod.js
index 5b1b7dd6439be4f0b721a17e683b1435c254f5b8..803762c16b9c7f484948171e42e1d8b4d95cbc7c 100644
--- a/dist/cli.cjs.prod.js
+++ b/dist/cli.cjs.prod.js
@@ -1,5 +1,5 @@
 "use strict";
-
+console.log()
 var meow = require("meow"), errors = require("@changesets/errors"), logger = require("@changesets/logger"), util = require("util"), fs = require("fs-extra"), path = require("path"), getPackages = require("@manypkg/get-packages"), getDependentsGraph = require("@changesets/get-dependents-graph"), config = require("@changesets/config"), chalk = require("chalk"), child_process = require("child_process"), termSize = require("term-size"), enquirer = require("enquirer"), externalEditor = require("external-editor"), ansiColors = require("ansi-colors"), git = require("@changesets/git"), writeChangeset = require("@changesets/write"), resolveFrom = require("resolve-from"), semver = require("semver"), outdent = require("outdent"), applyReleasePlan = require("@changesets/apply-release-plan"), readChangesets = require("@changesets/read"), assembleReleasePlan = require("@changesets/assemble-release-plan"), pre$1 = require("@changesets/pre"), pLimit = require("p-limit"), preferredPM = require("preferred-pm"), spawn = require("spawndamnit"), isCI = require("is-ci"), table = require("tty-table"), getReleasePlan = require("@changesets/get-release-plan");
 
 function _interopDefault(e) {
@@ -396,28 +396,25 @@ function getCorrectRegistry(packageJson) {
   return registry && "https://registry.yarnpkg.com" !== registry ? registry : "https://registry.npmjs.org";
 }
 
+const getPublishToolVersion = async (name, cwd) => (await spawn__default.default(name, [ "--version" ], {
+  cwd: cwd
+})).stdout.toString().trim();
+
 async function getPublishTool(cwd) {
-  const pm = await preferredPM__default.default(cwd);
-  if (!pm || "pnpm" !== pm.name) return {
-    name: "npm"
+  var _await$preferredPM;
+  const name = (null === (_await$preferredPM = await preferredPM__default.default(cwd)) || void 0 === _await$preferredPM ? void 0 : _await$preferredPM.name) || "npm", version = await getPublishToolVersion(name, cwd);
+  return "yarn" === name && semver__default.default.lt(version, "2.0.0") ? {
+    name: "npm",
+    version: await getPublishToolVersion("npm", cwd)
+  } : {
+    name: name,
+    version: version
   };
-  try {
-    let version = (await spawn__default.default("pnpm", [ "--version" ], {
-      cwd: cwd
-    })).stdout.toString().trim(), parsed = semver__default.default.parse(version);
-    return {
-      name: "pnpm",
-      shouldAddNoGitChecks: void 0 !== (null == parsed ? void 0 : parsed.major) && parsed.major >= 5
-    };
-  } catch (e) {
-    return {
-      name: "pnpm",
-      shouldAddNoGitChecks: !1
-    };
-  }
 }
 
 async function getTokenIsRequired() {
+  let publishTool = await getPublishTool(process.cwd());
+  if (publishTool.name === "yarn") return false;
   const envOverride = {
     npm_config_registry: getCorrectRegistry()
   };
@@ -432,8 +429,11 @@ async function getTokenIsRequired() {
 
 function getPackageInfo(packageJson) {
   return npmRequestLimit((async () => {
+    let publishTool = await getPublishTool(process.cwd());
     logger.info("npm info " + packageJson.name);
-    let result = await spawn__default.default("npm", [ "info", packageJson.name, "--registry", getCorrectRegistry(packageJson), "--json" ]);
+    let result = "yarn" === publishTool.name ? await spawn__default.default("yarn", [ "npm", "info", packageJson.name, "--json" ], {
+    env: Object.assign({}, process.env)
+  }) : await spawn__default.default("npm", [ "info", packageJson.name, "--registry", getCorrectRegistry(packageJson), "--json" ]);
     return "" === result.stdout.toString() ? {
       error: {
         code: "E404"
@@ -466,23 +466,36 @@ let otpAskLimit = pLimit__default.default(1), askForOtpCode = twoFactorState =>
   return twoFactorState.token = val, val;
 })), getOtpCode = async twoFactorState => null !== twoFactorState.token ? twoFactorState.token : askForOtpCode(twoFactorState);
 
+const isOtpError = error => "EOTP" === error.code || "E401" === error.code && error.detail.includes("--otp=<code>");
+
 async function internalPublish(pkgName, opts, twoFactorState) {
-  let publishTool = await getPublishTool(opts.cwd), publishFlags = opts.access ? [ "--access", opts.access ] : [];
-  if (publishFlags.push("--tag", opts.tag), await twoFactorState.isRequired && !isCI__default.default) {
+  let publishTool = await getPublishTool(opts.cwd), shouldHandleOtp = !isCI__default.default && ("yarn" !== publishTool.name || semver__default.default.gte(publishTool.version, "3.2.0-rc.8")), publishFlags = "yarn" !== publishTool.name ? [ "--json" ] : [];
+  if (opts.access && publishFlags.push("--access", opts.access), publishFlags.push("--tag", opts.tag), 
+  shouldHandleOtp && await twoFactorState.isRequired) {
     let otpCode = await getOtpCode(twoFactorState);
     publishFlags.push("--otp", otpCode);
   }
-  "pnpm" === publishTool.name && publishTool.shouldAddNoGitChecks && publishFlags.push("--no-git-checks");
+  "pnpm" === publishTool.name && semver__default.default.gte(publishTool.version, "5.0.0") && publishFlags.push("--no-git-checks");
   const envOverride = {
     npm_config_registry: getCorrectRegistry()
   };
-  let {code: code, stdout: stdout, stderr: stderr} = await spawn__default.default(publishTool.name, [ "publish", opts.cwd, "--json", ...publishFlags ], {
+  let {code: code, stdout: stdout, stderr: stderr} = "yarn" === publishTool.name ? await spawn__default.default("yarn", [ "npm", "publish", ...publishFlags ], {
+    cwd: opts.cwd,
+    env: Object.assign({}, process.env, envOverride)
+  }) : await spawn__default.default(publishTool.name, [ "publish", opts.cwd, ...publishFlags ], {
     env: Object.assign({}, process.env, envOverride)
   });
   if (0 !== code) {
+    if ("yarn" === publishTool.name) {
+      const output = stdout.toString().trim().split("\n").filter((line => !/YN0000:/.test(line))).join("\n");
+      return logger.error(`an error occurred while publishing ${pkgName}:`, "\n" + output), 
+      {
+        published: !1
+      };
+    }
     let json = getLastJsonObjectFromString(stderr.toString()) || getLastJsonObjectFromString(stdout.toString());
     if (null != json && json.error) {
-      if (("EOTP" === json.error.code || "E401" === json.error.code && json.error.detail.includes("--otp=<code>")) && !isCI__default.default) return null !== twoFactorState.token && (twoFactorState.token = null), 
+      if (shouldHandleOtp && ("EOTP" === (error = json.error).code || "E401" === error.code && error.detail.includes("--otp=<code>"))) return null !== twoFactorState.token && (twoFactorState.token = null), 
       twoFactorState.isRequired = Promise.resolve(!0), internalPublish(pkgName, opts, twoFactorState);
       logger.error(`an error occurred while publishing ${pkgName}: ${json.error.code}`, json.error.summary, json.error.detail ? "\n" + json.error.detail : "");
     }
@@ -490,6 +503,7 @@ async function internalPublish(pkgName, opts, twoFactorState) {
       published: !1
     };
   }
+  var error;
   return {
     published: !0
   };
diff --git a/dist/cli.esm.js b/dist/cli.esm.js
index ced46d2520d4bff71b94e4023b1841373a55712b..044d375056d5007859ee1a2e9c3dc7f0c1b4fcd2 100644
--- a/dist/cli.esm.js
+++ b/dist/cli.esm.js
@@ -682,31 +682,33 @@ function getCorrectRegistry(packageJson) {
   return !registry || registry === "https://registry.yarnpkg.com" ? "https://registry.npmjs.org" : registry;
 }
 
+const getPublishToolVersion = async (name, cwd) => (await spawn$1(name, ["--version"], {
+  cwd
+})).stdout.toString().trim();
+
 async function getPublishTool(cwd) {
-  const pm = await preferredPM(cwd);
-  if (!pm || pm.name !== "pnpm") return {
-    name: "npm"
-  };
+  var _await$preferredPM;
 
-  try {
-    let result = await spawn$1("pnpm", ["--version"], {
-      cwd
-    });
-    let version = result.stdout.toString().trim();
-    let parsed = semver.parse(version);
-    return {
-      name: "pnpm",
-      shouldAddNoGitChecks: (parsed === null || parsed === void 0 ? void 0 : parsed.major) === undefined ? false : parsed.major >= 5
-    };
-  } catch (e) {
+  const name = ((_await$preferredPM = await preferredPM(cwd)) === null || _await$preferredPM === void 0 ? void 0 : _await$preferredPM.name) || "npm";
+  const version = await getPublishToolVersion(name, cwd);
+
+  if (name === "yarn" && semver.lt(version, "2.0.0")) {
+    // Yarn Classic doesn't do anything special when publishing, let's stick to the npm client in such a case
     return {
-      name: "pnpm",
-      shouldAddNoGitChecks: false
+      name: "npm",
+      version: await getPublishToolVersion("npm", cwd)
     };
   }
+
+  return {
+    name,
+    version
+  };
 }
 
 async function getTokenIsRequired() {
+  let publishTool = await getPublishTool(process.cwd());
+  if (publishTool.name === "yarn") return false;
   // Due to a super annoying issue in yarn, we have to manually override this env variable
   // See: https://github.com/yarnpkg/yarn/issues/2935#issuecomment-355292633
   const envOverride = {
@@ -731,6 +733,7 @@ async function getTokenIsRequired() {
 }
 function getPackageInfo(packageJson) {
   return npmRequestLimit(async () => {
+    let publishTool = await getPublishTool(process.cwd());
     info(`npm info ${packageJson.name}`); // Due to a couple of issues with yarnpkg, we also want to override the npm registry when doing
     // npm info.
     // Issues: We sometimes get back cached responses, i.e old data about packages which causes
@@ -738,7 +741,9 @@ function getPackageInfo(packageJson) {
     // as they will always give a 404, which will tell `publish` to always try to publish.
     // See: https://github.com/yarnpkg/yarn/issues/2935#issuecomment-355292633
 
-    let result = await spawn$1("npm", ["info", packageJson.name, "--registry", getCorrectRegistry(packageJson), "--json"]); // Github package registry returns empty string when calling npm info
+    let result = publishTool.name === "yarn" ? await spawn$1("yarn", ["npm", "info", packageJson.name, "--json"], {
+    env: Object.assign({}, process.env)
+  }) : await spawn$1("npm", ["info", packageJson.name, "--registry", getCorrectRegistry(packageJson), "--json"]); // Github package registry returns empty string when calling npm info
     // for a non-existent package instead of a E404
 
     if (result.stdout.toString() === "") {
@@ -793,20 +798,32 @@ let getOtpCode = async twoFactorState => {
   }
 
   return askForOtpCode(twoFactorState);
+};
+
+const isOtpError = error => {
+  // The first case is no 2fa provided, the second is when the 2fa is wrong (timeout or wrong words)
+  return error.code === "EOTP" || error.code === "E401" && error.detail.includes("--otp=<code>");
 }; // we have this so that we can do try a publish again after a publish without
 // the call being wrapped in the npm request limit and causing the publishes to potentially never run
 
+
 async function internalPublish(pkgName, opts, twoFactorState) {
   let publishTool = await getPublishTool(opts.cwd);
-  let publishFlags = opts.access ? ["--access", opts.access] : [];
+  let shouldHandleOtp = !isCI && (publishTool.name === "yarn" ? semver.gte(publishTool.version, "3.2.0-rc.8") : true);
+  let publishFlags = publishTool.name !== "yarn" ? ["--json"] : [];
+
+  if (opts.access) {
+    publishFlags.push("--access", opts.access);
+  }
+
   publishFlags.push("--tag", opts.tag);
 
-  if ((await twoFactorState.isRequired) && !isCI) {
+  if (shouldHandleOtp && (await twoFactorState.isRequired)) {
     let otpCode = await getOtpCode(twoFactorState);
     publishFlags.push("--otp", otpCode);
   }
 
-  if (publishTool.name === "pnpm" && publishTool.shouldAddNoGitChecks) {
+  if (publishTool.name === "pnpm" && semver.gte(publishTool.version, "5.0.0")) {
     publishFlags.push("--no-git-checks");
   } // Due to a super annoying issue in yarn, we have to manually override this env variable
   // See: https://github.com/yarnpkg/yarn/issues/2935#issuecomment-355292633
@@ -819,21 +836,35 @@ async function internalPublish(pkgName, opts, twoFactorState) {
     code,
     stdout,
     stderr
-  } = await spawn$1(publishTool.name, ["publish", opts.cwd, "--json", ...publishFlags], {
+  } = publishTool.name === "yarn" ? await spawn$1("yarn", ["npm", "publish", ...publishFlags], {
+    cwd: opts.cwd,
+    env: Object.assign({}, process.env, envOverride)
+  }) : await spawn$1(publishTool.name, ["publish", opts.cwd, ...publishFlags], {
     env: Object.assign({}, process.env, envOverride)
   });
 
   if (code !== 0) {
-    // NPM's --json output is included alongside the `prepublish` and `postpublish` output in terminal
+    // yarn berry doesn't support --json and we don't attempt to parse its output to a machine-readable format
+    if (publishTool.name === "yarn") {
+      const output = stdout.toString().trim().split("\n") // this filters out "unnamed" logs: https://yarnpkg.com/advanced/error-codes/#yn0000---unnamed
+      // this includes a list of packed files and the "summary output" like: "Failed with errors in 0s 75ms"
+      // those are not that interesting so we reduce the noise by dropping them
+      .filter(line => !/YN0000:/.test(line)).join("\n");
+      error(`an error occurred while publishing ${pkgName}:`, `\n${output}`);
+      return {
+        published: false
+      };
+    } // NPM's --json output is included alongside the `prepublish` and `postpublish` output in terminal
     // We want to handle this as best we can but it has some struggles:
     // - output of those lifecycle scripts can contain JSON
     // - npm7 has switched to printing `--json` errors to stderr (https://github.com/npm/cli/commit/1dbf0f9bb26ba70f4c6d0a807701d7652c31d7d4)
     // Note that the `--json` output is always printed at the end so this should work
+
+
     let json = getLastJsonObjectFromString(stderr.toString()) || getLastJsonObjectFromString(stdout.toString());
 
     if (json !== null && json !== void 0 && json.error) {
-      // The first case is no 2fa provided, the second is when the 2fa is wrong (timeout or wrong words)
-      if ((json.error.code === "EOTP" || json.error.code === "E401" && json.error.detail.includes("--otp=<code>")) && !isCI) {
+      if (shouldHandleOtp && isOtpError(json.error)) {
         if (twoFactorState.token !== null) {
           // the current otp code must be invalid since it errored
           twoFactorState.token = null;
diff --git a/package.json b/package.json
index 0912d0f8fbf6f88c623ba4de0c87345947cde035..4ea6809033e063422e98cc3370685eb9b46847a2 100644
--- a/package.json
+++ b/package.json
@@ -57,7 +57,7 @@
     "meow": "^6.0.0",
     "outdent": "^0.5.0",
     "p-limit": "^2.2.0",
-    "preferred-pm": "^3.0.0",
+    "preferred-pm": "^3.0.3",
     "resolve-from": "^5.0.0",
     "semver": "^5.4.1",
     "spawndamnit": "^2.0.0",

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet