rushstack icon indicating copy to clipboard operation
rushstack copied to clipboard

[all] Rushstack CI blocked by "browerslist update" warning

Open elliot-nelson opened this issue 4 years ago • 38 comments

Summary

Looks like recent PR builds are failing due to some warnings (see this build).

--[ WARNING: heft-node-jest-tutorial ]-----------------------[ 8.29 seconds ]--

Browserslist: caniuse-lite is outdated. Please run:
npx browserslist@latest --update-db

Why you should do it regularly:
https://github.com/browserslist/browserslist#browsers-data-updating

--[ WARNING: heft-sass-test ]-------------------------------[ 16.42 seconds ]--

Browserslist: caniuse-lite is outdated. Please run:
npx browserslist@latest --update-db

Why you should do it regularly:
https://github.com/browserslist/browserslist#browsers-data-updating

It's not super clear how to fix this warning, running the suggested command in one of the affected projects doesn't work because there's not a lock file, and running it in the common/temp folder doesn't work because pnpm can't install packages there.

elliot-nelson avatar Oct 23 '21 13:10 elliot-nelson

This has come up a few times before in Zulip chat, for example:

browserslist --update-db

Browserlist Warning

It can be fixed by upgrading indirect dependencies. However fundamentally the browserslist hook is performing a nondeterministic check that will break everyone's ability to build all historical branches of your repo. This is really bad. In the past we recommended for people to avoid browserslist entirely, but that's not always feasible.

Let's use this incident to figure out the right way to disable the browserslist check, and then we can add it to the Rush FAQ.

CC @MickeyPhoenix whose team encountered this also IIRC

octogonz avatar Oct 23 '21 20:10 octogonz

The error is actually coming from this code in the browserslist NPM package:

browserslist/node.js

  oldDataWarning: function oldDataWarning (agentsObj) {
    if (dataTimeChecked) return
    dataTimeChecked = true
    if (process.env.BROWSERSLIST_IGNORE_OLD_DATA) return

    var latest = latestReleaseTime(agentsObj)
    var halfYearAgo = Date.now() - TIME_TO_UPDATE_CANIUSE

    if (latest !== 0 && latest < halfYearAgo) {
      console.warn(
        'Browserslist: caniuse-lite is outdated. Please run:\n' +
        '  npx browserslist@latest --update-db\n' +
        '  Why you should do it regularly: ' +
        'https://github.com/browserslist/browserslist#browsers-data-updating'
      )
    }
  },

octogonz avatar Oct 23 '21 20:10 octogonz

Nice! So there is an env var that could be set.

elliot-nelson avatar Oct 23 '21 20:10 elliot-nelson

The root cause is that they are calling console.warn() instead of console.log(). Someone already pointed that out in issue https://github.com/browserslist/browserslist/issues/361, but they fixed it by introducing this environment variable instead.

The fundamental conflict is that Rush considers console.warn() to be a warning, and warnings breaks CI builds. Having a warning arise due to clock measurements will cause developers to suddenly lose their ability to build old branches, which is a big deal for us.

If console.warn() cannot be eliminated, then perhaps Rush can hack around it by forcing BROWSERSLIST_IGNORE_OLD_DATA=1 for everyone by default. The behavior of Date.now() - TIME_TO_UPDATE_CANIUSE is incompatible with our use case.

@ai what do you think? Would you reconsider eliminating console.warn() in browserlist?

octogonz avatar Oct 23 '21 20:10 octogonz

So there is an env var that could be set.

We could also simply instruct Rush users to set BROWSERSLIST_IGNORE_OLD_DATA=1 themselves. But it's not a great experience. Every repo that encounters this will have to waste time investigating it and rediscovering the solution. And at HBO for example, we'll have to bulk-apply the BROWSERSLIST_IGNORE_OLD_DATA=1 to hundreds of pipeline files, and then make sure every future pipeline remembers to set it. The value of this validation is not worth that headache IMO.

octogonz avatar Oct 23 '21 20:10 octogonz

I would prefer if you didn't try to manage third-party libraries for me, that's unintuitive. Borwserslist isn't the only tool that does that, Prisma is similar, and honestly, even PNPM does that, it's just that it's not an issue during rush update (pnpm is .log, not .warn, I guess). I would put a warning in the docs saying "Some tools print update warnings, they usually provide an environment variable to silence it"

Faithfinder avatar Oct 23 '21 21:10 Faithfinder

Maybe with "here are the ones we know about" and put browserslist and prisma there

Faithfinder avatar Oct 23 '21 21:10 Faithfinder

I have an idea of not showing this warning in case where browsers query doesn't contain time related queries like last 2 versions.

How do you use Browserslist in this project? What is browsers query?

ai avatar Oct 23 '21 21:10 ai

In this particular repo, we don't use it at all actually. 🙂 It is coming in as an indirect dependency of other NPM packages such as webpack:

https://github.com/microsoft/rushstack/blob/94e2aa7df4a0f9f80a378cdffd67d5c8f3116258/common/config/rush/pnpm-lock.yaml#L17534

octogonz avatar Oct 23 '21 22:10 octogonz

@octogonz lets me rephrase. Why is Browserslist calling in your stack? Do you compile JS sourced with Babel or use Autoprefixer? (I see these dependencies in lock file, but I am not sure what is this tool about)

ai avatar Oct 23 '21 22:10 ai

This GitHub repo is a monorepo for a bunch of different build tools, and it also includes some sample projects that are built by the tools. The browserlist package probably gets invoked in multiple different ways.

Looking at the first warning in the log above, heft-node-jest-tutorial is a simple Node.js project that does not use Webpack at all. I can try running it in a debugger and see exactly where browserlist gets loaded. In most cases it's likely to be via an API.

octogonz avatar Oct 23 '21 22:10 octogonz

For this one, looks like Jest is invoking Babel, and then Babel queries browserslist:

image

The query returns an empty array. The heft-node-jest-tutorial as no browser configuration itself and would never run in a web browser, but apparently Jest is still performing the queries.

octogonz avatar Oct 23 '21 22:10 octogonz

For comparison, here's how HBO's internal monorepo invokes browserslist:

import browserslist from 'browserslist';

function getTargetsFromQuery(query: string[]): string[] {
  try {
    return browserslist(query);
  } catch (error) {
    throw new Error(`Browsers list query for '${query}' failed: ${error.message}`);
  }
}

This is from a specialized tool that builds the 4 standardized polyfill bundles that are shared across all the different app projects and their library projects. This tool has its own JSON input files, and then it generates a query which is passed to browserslist.

So in both cases, a specialized tool is querying the browserslist API; there is no .browserslistrc. (However I'm sure plenty of other Rush consumers do use .browserslistrc in the normal way.)

octogonz avatar Oct 23 '21 22:10 octogonz

For the case of Jest BROWSERSLIST_IGNORE_OLD_DATA can be safely used. I personally, think that they should pass exact Node.js version to Babel and avoid calling Browserslist for default browsers list, since it reason less (but I am not sure how to fix it).

This outdated caniuse-lite warning is important for building web pages. People can avoid updating caniuse-lite for 2-3 years, which will lead to very old browsers in Babel and Autoprefixer and increase JS/CSS files by unnecessary polyfills and slow down the website.

ai avatar Oct 23 '21 22:10 ai

This outdated caniuse-lite warning is important for building web pages. People can avoid updating caniuse-lite for 2-3 years, which will lead to very old browsers in Babel and Autoprefixer and increase JS/CSS files by unnecessary polyfills and slow down the website.

Sure, but we should start by agreeing that determinism is an indispensable requirement for professional app development: If two people do git checkout for a given Git hash, and build that code, they should expect to get equivalent output including any errors/warnings, regardless of what day it is. Without this, troubleshooting build failures is a nightmare. (It is the reason why everyone uses package-lock.json or pnpm-lock.yaml for example.)

For the Rush ecosystem, we have a second consideration that console.warn() is interpreted as a warning. And warnings break a CI build. We can't change this.

So if we want to print time-based notices (and I understand why that's important to you), there seem to be basically only two ways to accomplish that:

  1. The notices are printed with console.log() and treated as purely informational. This is how PNPM warns about old versions. -OR-
  2. The notices are printed with console.warn() but are based on Git-tracked inputs rather than the system clock.

For option 2, here's one possible idea:

  • browserslist has an NPM dependency like "browserslist-latest": "*"
  • The oldDataWarning() check imports a time value or version number from the browserslist-latest dependency and compares against that
  • This way the warning /is/ time based, but its calculation only changes when package-lock.json is updated (because npm install selects a newer version for "browserslist-latest": "*" )

It's just one idea though.

octogonz avatar Oct 23 '21 22:10 octogonz

Sure, but we should start by agreeing that determinism is an indispensable requirement for professional app development

Absolutely. This is why Browserslist is taken browsers data from the same lock file.

This warning just promote to update caniuse-lite version once every 3-6 months. Between these updates, browsers will be the same between all developers.

We have a problem of falling CI on warnings. It is strange behavior for me since it delete the difference between the error and warning.

The notices are printed with console.log() and treated as purely informational

It will break more system. For instance, you can call a tool with JSON output and this warning will break stdout output.

The notices are printed with console.warn() but are based on Git-tracked inputs rather than the system clock.

It will remove the whole idea of these warnings.

I have another idea. We can think about disabling this warning on process.env.CI since the warning should be readable by user. On CI, nobody sees the warning for most of the cases.

But warning still be shown on non-CI environments.

ai avatar Oct 23 '21 22:10 ai

We have a problem of falling CI on warnings. It is strange behavior for me since it delete the difference between the error and warning.

Rush's difference is very well-defined:

  • Errors are critical issues that prevent downstream tasks/projects from running. For example, if your project does import { X } from 'y' and y does not exist, we can't continue.
  • Warnings do NOT prevent downstream tasks/projects from running. They are issues that developers can safely ignore while working (e.g. a lint issue).

Under no circumstances do we allow a PR to be merged with warnings. In a large ecosystem, if you allow that, then your build will quickly accumulate so many "warnings" that everyone ignores them, which defeats the purpose of having warnings. If a person wants to ignore a warning, they must suppress it somehow (e.g. eslint-disable-line) so that other developers don't see the warning in their logs.

The notices are printed with console.warn() but are based on Git-tracked inputs rather than the system clock.

It will remove the whole idea of these warnings.

Why is that? If a branch is under active development, then someone will inevitably need to upgrade an NPM package, and then they will see your warning. If a branch is not under active development, then the warning would only cause trouble.

But warning still be shown on non-CI environments.

This doesn't seem like a great solution. It's still going to cause trouble for developers who pull an old branch and try to build it, and then wonder why Rush is reporting warnings.

octogonz avatar Oct 23 '21 23:10 octogonz

Why is that? If a branch is under active development, then someone will inevitably need to upgrade an NPM package, and then they will see your warning. If a branch is not under active development, then the warning would only cause trouble.

Even if you are not changing anything in website, it is a good idea to update browsers to reduce JS/CSS files size.

(Also, adding git request will be hard and having git call during Browserslist call will be unexpected for users).

ai avatar Oct 23 '21 23:10 ai

(Also, adding git request will be hard and having git call during Browserslist call will be unexpected for users).

Maybe you've misunderstood my proposal. As an extremely simplified example, imagine a new NPM package like this:

browserslist-latest/package.json

{
  "name": "browserslist-latest",
  "version": "1.2.3",
  "currentDate": "10/1/2021"
}

Now imagine this dependency relationship:

browserslist-latest/package.json

{
  "name": "browserslist",
  "dependencies": {
    "browserslist-latest": "*"
  }
}

During npm install, the current latest version of the browserslist-latest package will get installed to match "*". So if oldDataWarning() calls require("browserslist-latest/package.json").currentDate the value will be "10/1/2021".

Now suppose that every month we publish a new version of browserslist-latest, for example 1.2.4 has the date "11/1/2021" and 1.2.5 has the date "12/1/2021". In this way, the oldDataWarning() function can still compare against a date, but the date value is 100% deterministic: It only changes when someone upgrades their NPM packages. These upgrades are reasonably frequent for any actively developed project.

(The actual implementation could be different, maybe using a counter or version number instead of currentDate. This is just a thought experiment.)

octogonz avatar Oct 23 '21 23:10 octogonz

During npm install, the current latest version of the browserslist-latest package will get installed to match "*"

Am I right that npm uses lock file by default? This is why * will be always the same.

In the project where nobody all npm update for years, it will not warn developers of very old polyfills (because of old browsers from Browserslist).

ai avatar Oct 23 '21 23:10 ai

I think the "*" is actually part of the problem.

Browserslist is not unique, in my mind, in having important updates; if it's a year old that's not great, but you could say that about your version of react or webpack or jest etc as well. None of those packages start warning me if I decide not to update.

What makes browserslist tricky compared to those other dependencies is that I can tell that they are out of date, whereas the "*" dependency makes that harder to tell - it tucks the version away into the lock file instead of making it a normal version update like any other library.

That's the point of the suggestion @octogonz was making -- if you could have a package that was updated once a month or every quarter, with a standard version number, then really you could eliminate both the warning AND the custom update script, because now you are updateable just like a webpack or a typescript.

elliot-nelson avatar Oct 24 '21 00:10 elliot-nelson

Browserslist is not unique, in my mind, in having important updates; if it's a year old that's not great, but you could say that about your version of react or webpack or jest etc as well. None of those packages start warning me if I decide not to update.

The unique feature of Browserslist is that user explicitly ask for the latest browsers. If developer put last 2 version in .browserslistrc, they expect to have actual versions. Autoprefixer promises to add only actual prefixes (even with no .browserlistrc with default browsers).

There is another important difference: regular updates (without API breaking changes) of Jest, webpack, or React will not (in most of the cases) make your website faster. We have this warning to make Internet in general a better place (at least a little faster).

ai avatar Oct 24 '21 00:10 ai

@ai Maybe there's two distinct situations then: where the user is intentionally using the features browserslist provides, and where it's a buried dependency they aren't even aware of (like us).

Your earlier suggestion where browserslist won't report the warning unless the query contains certain elements might solve the problem for both: intentional users get notified, and unintentional uses would never see a warning.

elliot-nelson avatar Oct 24 '21 01:10 elliot-nelson

I have an idea of not showing this warning in case where browsers query doesn't contain time related queries like last 2 versions.

Agreed -- maybe this ☝ is the best compromise.

If last 2 versions is a moving target that changes over time (for a given Git hash), then people who care strongly about determinism wouldn't use that setting, and thus won't be impacted by the warning.

And since the Rush Stack and HBO monorepos both are not using .browserslistrc settings (but rather invoking the tool as a library API), then it would solve the problem for us as well.

(It's in fact questionable whether a library API should be printing stuff to console.warn() in the first place -- really it should return the warnings as an array for the caller to handle.)

octogonz avatar Oct 24 '21 01:10 octogonz

Your earlier suggestion where browserslist won't report the warning unless the query contains certain elements might solve the problem for both: intentional users get notified, and unintentional uses would never see a warning.

I am not sure that it will solve all problems because on no .browserslistrc (and if tool like Jest do not override default value) Browserslist will use default query which uses time-based queries (last 2 versions).

But we can try. I will need a PR for this because I am working on another open source projects right now.

ai avatar Oct 24 '21 01:10 ai

I would normally expect warnings about package versioning to occur while running package manager operations, e.g. to warn during install, rather than at runtime. In other words "hey, you just installed an ancient version of this package" as opposed to "you're trying to run an ancient version of this package"

dmichon-msft avatar Oct 25 '21 16:10 dmichon-msft

Looking closer at the call stack, it's not obvious to me if there is an easy fix in browserslist. The way I take @ai's suggestion is to change this line to something like this:

  if (/* isNotTimeBased(queries) */) {
    env.oldDataWarning(browserslist.data)
  }

But the implementation of "is not timed based" doesn't seem straightforward. You could specifically check if any of the queries are in the form last 1 year or last 2 versions, but those are only the most obvious. >0.5% is just as time-based (a version used at 2% today won't be in three months). So is "Firefox ESR" (the meaning of ESR changes over time).

We might be able to come up with a list of "allowed queries" that don't trigger a warning (like ["firefox 27"] or ["node 12"] probably don't need warnings). But then the behavior of browserslist itself is unpredictable and confusing to users. It would be better I think if the API of browserslist itself exposed the warning behavior as an option, so it was obvious to callers what the behavior will be:

// Default (warn after 180 days)
browserslist(queries, { oldDataWarningDays: 180 })

// Make warning more aggressive
browserslist(queries, { oldDataWarningDays: 30 })

// Disable warning
browserslist(queries, { oldDataWarningDays: 0 })

This change would keep the current desired behavior, but we could find and propose changes to internal or external tooling that is using browserslist that shouldn't generate warnings.

elliot-nelson avatar Oct 25 '21 16:10 elliot-nelson

There may be a separate ticket to consider here, to make browserslist (caniuse) easier to update in a monorepo.

For the rushstack project, it's obvious that we would never want an outdated caniuse dependency to block CI. But that doesn't mean it's a given for every Rush monorepo maintainer: if all you do is push out large websites, maybe you DO want that warning, and your response to it is to immediately update caniuse.

But, it might be unclear to them how to do so, since the command suggested by the warning doesn't work in a rush monorepo.

elliot-nelson avatar Oct 25 '21 16:10 elliot-nelson

Having fresh caniuse-lite is the best option. Warning is trying to help here. It is not a false alarm.

ai avatar Oct 25 '21 16:10 ai

I do lean toward what @dmichon-msft said here regarding when/where the warning appears. IMO since this is in relation to a package being out of date, it would make the most sense to perform this check during the install as a post-install script. Given that the check is still based on how old the agent itself is (which is provided during install from caniuse-lite), as well as the fact that modifying the install is the only way to mitigate the issue itself, it seems to make sense to place here (especially since it's the only non-runtime place where this type of check can be performed, AFAIK?). We also see a similar pattern for other packages that have issues with dependency versions (ex. node-gyp and Python versions).

Comparing against the date is sub-optimal because of the lack of determinism, but also because it assumes that the package will be maintained and updated for the rest of time forever.... but that does seem to be the general point of the package, and maybe that's fine in this case.

D4N14L avatar Oct 25 '21 19:10 D4N14L