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

[rush] deferredInstallationScripts & --ignore-scripts #3537

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

Conversation

chengcyber
Copy link
Contributor

@chengcyber chengcyber commented Jul 14, 2022

Summary

This PR implements "two stage install" proposal.
close #3530

Details

This feature is now behind a flag in experiments.json: "deferredInstallationScripts": true

Workflow now:

  1. Dividing "rush install/update" into two stage install when using pnpm & "useWorkspaces" on implicitly.
  2. During two stage install, '--ignore-scripts' is pushed as a cli parameter to "pnpm install".
  3. After "pnpm install" successfully, if "--ignore-scripts" is not specified explicitly by user, run "pnpm rebuild --pending"

Goal: faster retries for install lifecycle scripts:
If step3 fails, such as encountering an error in a "postinstall" scripts, rerun "rush install" will skip step2, which gets a faster retry on failed scripts.

How it was tested

Use rushstack repo for example

  1. rush --debug install -t rush

2022-07-14 at 3 29 PM

two stage install works :)

  1. rush --debug install -t rush-lib

Because -t rush-lib is a subset of -t rush, stage1(pnpm install w/ --ignore-scripts) was skipped, and stage2(pnpm rebuild --pending) was run. At the same time, there is no pending rebuild scripts to run. rebuild finished in seconds.

2022-07-14 at 3 32 PM

@chengcyber chengcyber marked this pull request as ready for review July 14, 2022 07:38
} else {
// full install
if (newState.selectedProjectNames === oldState.selectedProjectNames) {
return true;
Copy link
Contributor

Choose a reason for hiding this comment

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

It's bad for maintenance to have return true statements short circuiting the "does this object match" checker. The only early returns should be return false, and return true is the result when all the individual checks have passed.

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 agree with you. but i have no idea how to do it here. Could you give some suggestions on how to change this part of code for better maintenance.

libraries/rush-lib/src/cli/actions/InstallAction.ts Outdated Show resolved Hide resolved
libraries/rush-lib/src/cli/actions/UpdateAction.ts Outdated Show resolved Hide resolved
libraries/rush-lib/src/logic/base/BaseInstallManager.ts Outdated Show resolved Hide resolved
@@ -372,6 +376,10 @@ export class WorkspaceInstallManager extends BaseInstallManager {
}

protected async postInstallAsync(): Promise<void> {
if (this.twoStageInstall) {
this.commonTempInstallFlag.saveIfModified();
Copy link
Member

Choose a reason for hiding this comment

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

The use of this flag file feels like it's getting muddled by this change. It used to mean "everything in the repo is installed and ready to use", now it means "some subset of the repo is installed and possibly the post-install scripts have been run".

It would make sense to use this file to do more specific state tracking (I like that you're including the scope in the file now). We should also care about running the post-install scripts though since not running them could leave your repo in a less-usable state. Maybe we should check this file, and if ignoreScripts is true and this.options.ignoreScripts is false, we execute this stage, then write ignoreScripts: false in the flag file?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It kind of complicated. Say we have run rush install --ignore-scripts --to rush, the flag file records:

"selectedProjects": ["<other_deps>", ..., "rush-lib", "rush"]
"ignoreScripts": true

Now, if i run rush install --to rush-lib, the stage 1 pnpm install --ignore-scripts should be skipped, since --to rush-lib is a subset of --to rush. And Rush just run stage 2 pnpm rebuild --filter rush-lib.... Now we can not mark ignoreScripts: false in the flag file. Since the selectedProjects contains "rush", which is out of range of --filter rush-lib...

One possible solution maybe is to record more information in one selected project in the flag file, use the above case as an example, the flag file will be

"selectedProjects": [
// ... other deps
{
  packageName: 'rush-lib',
  ignoreScripts: false,
},
{
  packageName: 'rush',
  ignoreScripts: true,  // <--- because we run "pnpm rebuild --pending --filter rush-lib..." which excludes "rush"
}
]

In this way, in postinstallAsync we need calculate ignoreScripts based on all selectedProjects to figure out whether we can skip "pnpm rebuild --pending"...

Do you think it's worth the efforts to implement this logic? For now, i choose an easy implementation which run "pnpm rebuild --pending [--filter <project...>]" everytime. --pending makes pnpm to decide whether there is any script we need to run.

Copy link
Member

Choose a reason for hiding this comment

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

Good question. I think keeping more detailed info on the selected projects does make sense... it allows you to be very specific about the work you're performing and when you want to perform that work. Another option IMO would be to execute the install from scratch again (ie. running pnpm install --ignore-scripts ... then pnpm rebuild ...) whenever non-scoping arguments to the install have changed.

Basically, I feel like we either need to commit to tracking each project install on a per-project basis, or we need to treat all other options as something that could affect all projects and start from scratch.

throw new Error(`Encounter an error when running install lifecycle scripts. error: ${err.message}`);
} finally {
// Always save after pnpm rebuild to update timestamp of last install flag file.
this.commonTempInstallFlag.create();
Copy link
Member

Choose a reason for hiding this comment

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

Previously, this flag would only exist if there was a successful install. Use should be updated here to keep in line with what I describe above (update the flag for ignoreScripts in the case that the post-install scripts run + succeed, or leave as-is and use it to skip the install but use to execute post-install scripts.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The reason why invoking creating last install flag here: canSkipInstall compares a timestamp(mtime of this flag file) with mtime of node_modules folder. And no matter pnpm rebuild --pending runs successfully or not, node_modules folder is touched, i.e. mtime updated. So we need to update the mtime of this flag file to make the canSkipInstall works again.

Copy link
Member

Choose a reason for hiding this comment

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

Right, but skipping an install in the event of an error doesn't make sense. It should be updated only after the command is successfully executed. If the command fails, then the state of the repo is unknown and the last-install flag should probably be deleted.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's kindof fight against the main point of the proposal, faster retries on postinstall scripts.

Imagine

pnpm install --ignore-scripts --filter <a...>

👆 success

pnpm rebuild --pending --filter <a...>

👆 fail

In this case, if Rush.js doesn't update the last-install.flag. The "canSkipInstall" will never return true in this case. which triggers a "pnpm install --ignore-scripts --filter <a...>" again in next "rush install/update"...

but, the main point is to make "canSkipInstall" returns true in this case, and only the stage 2 command("pnpm rebuild") runs, which is the "faster retires" part...

Copy link
Member

Choose a reason for hiding this comment

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

Then in that case it should write the flag file twice, once after performing the install (with ignoreScripts: true), then once after running the post-install scripts (with ignoreScripts: false). We shouldn't be saying "yes, everything is installed correctly" regardless of success or failure, that defeats the entire purpose of the flag file.

Copy link
Collaborator

Choose a reason for hiding this comment

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

in that case it should write the flag file twice, once after performing the install

+1 Rush install is meant to be transactionally safe. It is a common optimization for CI agents to reuse the same git clone working folder from a previous job. And it is very common for the previous rush install to have been terminated because someone pushed an update to their PR branch, which causes the previous CI job to get immediately cancelled and restarted.

@chengcyber chengcyber changed the title feat(rush-lib): two stage install & --ignore-scripts feat(rush-lib): deferredInstallationScripts & --ignore-scripts Jul 16, 2022
@chengcyber chengcyber changed the title feat(rush-lib): deferredInstallationScripts & --ignore-scripts [rush] deferredInstallationScripts & --ignore-scripts Jul 16, 2022
@iclanton iclanton added this to In Progress in Bug Triage Jul 18, 2022
@chengcyber chengcyber force-pushed the feat-mitigation-postinstall-failures branch from 0392f2e to d1529e9 Compare July 19, 2022 09:54
* If true, rush install or rush update implicitly specify --ignore-scripts during pnpm install,
* and run install lifecycle scripts by pnpm rebuild --pending after pnpm install successfully.
*/
/*[LINE "HYPOTHETICAL"]*/ "deferredInstallationScripts": true
Copy link
Member

Choose a reason for hiding this comment

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

Will this eventually be our recommended approach?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes, I think so

libraries/rush-lib/src/api/LastInstallFlag.ts Outdated Show resolved Hide resolved
libraries/rush-lib/src/api/LastInstallFlag.ts Outdated Show resolved Hide resolved
libraries/rush-lib/src/api/LastInstallFlag.ts Show resolved Hide resolved
libraries/rush-lib/src/api/LastInstallFlag.ts Outdated Show resolved Hide resolved
libraries/rush-lib/src/api/LastInstallFlag.ts Outdated Show resolved Hide resolved
@chengcyber chengcyber force-pushed the feat-mitigation-postinstall-failures branch from d1529e9 to 7fa7ee2 Compare August 7, 2022 16:15
@chengcyber
Copy link
Contributor Author

Updates 2022/8/7:

  1. rebase latest main branch
  2. rename pushRebuildArgs to pushPnpmRebuildCommandArgs
  3. add a comment on Stage 1
  4. throw error when specifying --ignore-scripts in non-pnpm repos
  5. refactor last-install flag and add a individual isSelectedProjectInstalled method

@chengcyber
Copy link
Contributor Author

Update: Sync this branch with the latest main branch

"changes": [
{
"packageName": "@microsoft/rush",
"comment": "Supports deferredInstallationScripts behind a experimental flag, which makes faster retires for install lifecycle scripts",
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
"comment": "Supports deferredInstallationScripts behind a experimental flag, which makes faster retires for install lifecycle scripts",
"comment": "Add support for a `--deferredInstallationScripts` CLI flag behind an experimental flag, which allows for faster retires of dependency install lifecycle scripts.",

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually, there is no --defereredInstallScripts CLI flag. When turn on the experimental flag, post install scripts are invoked after pnpm install during "rush install/update"...

Dose "Add support for deferred running installation scripts behind an experimental flag, which allows for faster retires of dependency install lifecycle scripts." work for you?

@chengcyber
Copy link
Contributor Author

Update:

  1. merge the latest main branch
  2. update change log comment

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: In Progress
Bug Triage
  
In Progress
Development

Successfully merging this pull request may close these issues.

[rush] Proposal: Rush mitigations for postinstall failures
5 participants